Completed
Push — 16.1 ( 5f2316...9cf445 )
by Nathan
46:02 queued 28:38
created

et2_extension_nextmatch.js ➔ ???   B

Complexity

Conditions 1
Paths 0

Size

Total Lines 2227

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 0
nop 0
dl 0
loc 2227
rs 8.2857
c 0
b 0
f 0

48 Functions

Rating   Name   Duplication   Size   Complexity  
B et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.getSelection 0 8 5
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.resize 0 9 2
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.onselect 0 7 2
B et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._getSubgrid 0 30 1
B et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.init 0 51 5
C et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.transformAttributes 0 30 7
D et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.refresh 0 94 28
F et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.applyFilters 0 126 24
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.destroy 0 19 2
B et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._getColumnName 0 16 7
B et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._getInitialOrder 0 26 5
F et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._selectColumnsClick 0 197 15
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.resetSort 0 13 2
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._parseGrid 0 17 4
F et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._updateUserPreferences 0 97 21
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._genColumnCaption 0 20 2
F et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._getPreferences 0 72 16
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.doLoadingFinished 0 60 3
C et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._parseDataRow 0 79 12
F et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._parseHeaderRow 0 122 21
C et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._applyUserPreferences 0 80 18
B et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.sortBy 0 44 6
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.getValue 0 15 2
C et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.handle_drop 0 76 10
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._set_lettersearch 0 12 2
C et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_template 0 111 7
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_header_right 0 3 1
B et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_actions 0 13 5
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_no_filter 0 18 4
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_disabled 0 10 3
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_no_filter2 0 3 1
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_view 0 12 2
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._get_autorefresh 0 5 1
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_hide_header 0 3 2
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.isValid 0 1 1
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_header_left 0 3 1
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.resetDirty 0 1 1
B et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend._set_autorefresh 0 45 4
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_header_row 0 3 1
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_onfiledrop 0 3 1
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.isDirty 0 1 1
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_value 0 4 1
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_filter2 0 11 1
A et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_filter 0 11 1
C et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.set_columns 0 60 13
B et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.afterPrint 0 38 5
D et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.beforePrint 0 207 15
C et2_extension_nextmatch.js ➔ ... ➔ et2_DOMWidget.extend.getDOMNode 0 25 11

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
/**
2
 * EGroupware eTemplate2 - JS Nextmatch object
3
 *
4
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
5
 * @package etemplate
6
 * @subpackage api
7
 * @link http://www.egroupware.org
8
 * @author Andreas Stöckel
9
 * @copyright Stylite 2011
10
 * @version $Id$
11
 */
12
13
/*egw:uses
14
15
	// Include the action system
16
	egw_action.egw_action;
17
	egw_action.egw_action_popup;
18
	egw_action.egw_action_dragdrop;
19
	egw_action.egw_menu_dhtmlx;
20
21
	// Include some core classes
22
	et2_core_widget;
23
	et2_core_interfaces;
24
	et2_core_DOMWidget;
25
26
	// Include all widgets the nextmatch extension will create
27
	et2_widget_template;
28
	et2_widget_grid;
29
	et2_widget_selectbox;
30
	et2_widget_selectAccount;
31
	et2_widget_taglist;
32
	et2_extension_customfields;
33
34
	// Include all nextmatch subclasses
35
	et2_extension_nextmatch_controller;
36
	et2_extension_nextmatch_rowProvider;
37
	et2_extension_nextmatch_dynheight;
38
39
	// Include the grid classes
40
	et2_dataview;
41
42
*/
43
44
/**
45
 * Interface all special nextmatch header elements have to implement.
46
 */
47
var et2_INextmatchHeader = new Interface({
48
49
	/**
50
	 * The 'setNextmatch' function is called by the parent nextmatch widget
51
	 * and tells the nextmatch header widgets which widget they should direct
52
	 * their 'sort', 'search' or 'filter' calls to.
53
	 *
54
	 * @param {et2_nextmatch} _nextmatch
55
	 */
56
	setNextmatch: function(_nextmatch) {}
57
});
58
59
var et2_INextmatchSortable = new Interface({
60
61
	setSortmode: function(_mode) {}
62
63
});
64
65
/**
66
 * Class which implements the "nextmatch" XET-Tag
67
 *
68
 * NM header is build like this in DOM
69
 *
70
 * +- nextmatch_header -----+------------+----------+--------+---------+--------------+-----------+-------+
71
 * + header_left | search.. | header_row | category | filter | filter2 | header_right | favorites | count |
72
 * +-------------+----------+------------+----------+--------+---------+--------------+-----------+-------+
73
 *
74
 * everything left incl. standard filters is floated left:
75
 * +- nextmatch_header -----+------------+----------+--------+---------+
76
 * + header_left | search.. | header_row | category | filter | filter2 |
77
 * +-------------+----------+------------+----------+--------+---------+
78
 * everything from header_right on is floated right:
79
 *                                          +--------------+-----------+-------+
80
 *                                          | header_right | favorites | count |
81
 *                                          +--------------+-----------+-------+
82
 * @augments et2_DOMWidget
83
 */
84
var et2_nextmatch = (function(){ "use strict"; return et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrint],
85
{
86
	attributes: {
87
		// These normally set in settings, but broken out into attributes to allow run-time changes
88
		"template": {
89
			"name": "Template",
90
			"type": "string",
91
			"description": "The id of the template which contains the grid layout."
92
		},
93
		"hide_header": {
94
			"name": "Hide header",
95
			"type": "boolean",
96
			"description": "Hide the header",
97
			"default": false
98
		},
99
		"header_left": {
100
			"name": "Left custom template",
101
			"type": "string",
102
			"description": "Customise the nextmatch - left side.  Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change.  Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.",
103
			"default": ""
104
		},
105
		"header_right": {
106
			"name": "Right custom template",
107
			"type": "string",
108
			"description": "Customise the nextmatch - right side.  Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change.  Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.",
109
			"default": ""
110
		},
111
		"header_row": {
112
			"name": "Inline custom template",
113
			"type": "string",
114
			"description": "Customise the nextmatch - inline, after row count.  Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change.  Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.",
115
			"default": ""
116
		},
117
		"no_filter": {
118
			"name": "No filter",
119
			"type": "boolean",
120
			"description": "Hide the first filter",
121
			"default": et2_no_init
122
		},
123
		"no_filter2": {
124
			"name": "No filter2",
125
			"type": "boolean",
126
			"description": "Hide the second filter",
127
			"default": et2_no_init
128
		},
129
		"view": {
130
			"name": "View",
131
			"type": "string",
132
			"description": "Display entries as either 'row' or 'tile'.  A matching template must also be set after changing this.",
133
			"default": et2_no_init
134
		},
135
		"onselect": {
136
			"name": "onselect",
137
			"type": "js",
138
			"default": et2_no_init,
139
			"description": "JS code which gets executed when rows are selected.  Can also be a app.appname.func(selected) style method"
140
		},
141
		"onfiledrop": {
142
			"name": "onFileDrop",
143
			"type": "js",
144
			"default": et2_no_init,
145
			"description": "JS code that gets executed when a _file_ is dropped on a row.  Other drop interactions are handled by the action system.  Return false to prevent the default link action."
146
		},
147
		"settings": {
148
			"name": "Settings",
149
			"type": "any",
150
			"description": "The nextmatch settings",
151
			"default": {}
152
		}
153
	},
154
155
	legacyOptions: ["template","hide_header","header_left","header_right"],
156
	createNamespace: true,
157
158
	columns: [],
159
160
	// Current view, either row or tile.  We store it here as controllers are
161
	// recreated when the template changes.
162
	view: 'row',
163
164
	/**
165
	 * Constructor
166
	 *
167
	 * @memberOf et2_nextmatch
168
	 */
169
	init: function() {
170
		this._super.apply(this, arguments);
171
		this.activeFilters = {col_filter:{}};
172
173
		// Directly set current col_filters from settings
174
		jQuery.extend(this.activeFilters.col_filter, this.options.settings.col_filter);
175
176
		/*
177
		Process selected custom fields here, so that the settings are correctly
178
		set before the row template is parsed
179
		*/
180
		var prefs = this._getPreferences();
181
		var cfs = {};
182
		for(var i = 0; i < prefs.visible.length; i++)
183
		{
184
			if(prefs.visible[i].indexOf(et2_nextmatch_customfields.prototype.prefix) == 0)
185
			{
186
				cfs[prefs.visible[i].substr(1)] = !prefs.negated;
187
			}
188
		}
189
		var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~');
190
		if(typeof global_data == 'object' && global_data != null)
191
		{
192
			global_data.fields = cfs;
193
		}
194
195
		this.div = jQuery(document.createElement("div"))
196
			.addClass("et2_nextmatch");
197
198
199
		this.header = et2_createWidget("nextmatch_header_bar", {}, this);
200
		this.innerDiv = jQuery(document.createElement("div"))
201
			.appendTo(this.div);
202
203
		// Create the dynheight component which dynamically scales the inner
204
		// container.
205
		this.dynheight = new et2_dynheight(this.getInstanceManager().DOMContainer,
206
				this.innerDiv, 100);
207
208
		// Create the outer grid container
209
		this.dataview = new et2_dataview(this.innerDiv, this.egw());
210
211
		// Blank placeholder
212
		this.blank = jQuery(document.createElement("div"))
213
			.appendTo(this.dataview.table);
214
215
		// We cannot create the grid controller now, as this depends on the grid
216
		// instance, which can first be created once we have the columns
217
		this.controller = null;
218
		this.rowProvider = null;
219
	},
220
221
	/**
222
	 * Destroys all
223
	 */
224
	destroy: function() {
225
		// Stop autorefresh
226
		if(this._autorefresh_timer)
227
		{
228
			window.clearInterval(this._autorefresh_timer);
229
			this._autorefresh_timer = null;
230
		}
231
		// Unbind handler used for toggling autorefresh
232
		jQuery(this.getInstanceManager().DOMContainer.parentNode).off('show.et2_nextmatch');
233
		jQuery(this.getInstanceManager().DOMContainer.parentNode).off('hide.et2_nextmatch');
234
235
		// Free the grid components
236
		this.dataview.free();
237
		this.rowProvider.free();
238
		this.controller.free();
239
		this.dynheight.free();
240
241
		this._super.apply(this, arguments);
242
	},
243
244
	/**
245
	 * Loads the nextmatch settings
246
	 *
247
	 * @param {object} _attrs
248
	 */
249
	transformAttributes: function(_attrs) {
250
		this._super.apply(this, arguments);
251
252
		if (this.id)
253
		{
254
			var entry = this.getArrayMgr("content").data;
255
			_attrs["settings"] = {};
256
257
			if (entry)
258
			{
259
				_attrs["settings"] = entry;
260
261
				// Make sure there's an action var parameter
262
				if(_attrs["settings"]["actions"] && !_attrs.settings["action_var"])
263
				{
264
					_attrs.settings.action_var = "action";
265
				}
266
267
				// Merge settings mess into attributes
268
				for(var attr in this.attributes)
269
				{
270
					if(_attrs.settings[attr])
271
					{
272
						_attrs[attr] = _attrs.settings[attr];
273
						delete _attrs.settings[attr];
274
					}
275
				}
276
			}
277
		}
278
	},
279
280
	doLoadingFinished: function() {
281
		this._super.apply(this, arguments);
282
283
		// Register handler for dropped files, if possible
284
		if(this.options.settings.row_id)
285
		{
286
			// Appname should be first part of the template name
287
			var split = this.options.template.split('.');
288
			var appname = split[0];
289
290
			// Check link registry
291
			if(this.egw().link_get_registry(appname))
292
			{
293
				var self = this;
294
				// Register a handler
295
				jQuery(this.div)
296
					.on('dragenter','.egwGridView_grid tr',function(e) {
297
						// Figure out _which_ row
298
						var row = self.controller.getRowByNode(this);
299
300
						if(!row || !row.uid)
301
						{
302
							return false;
303
						}
304
						e.stopPropagation(); e.preventDefault();
305
306
						// Indicate acceptance
307
						if(row.controller && row.controller._selectionMgr)
308
						{
309
							row.controller._selectionMgr.setFocused(row.uid,true);
310
						}
311
						return false;
312
					})
313
					.on('dragexit','.egwGridView_grid tr', function(e) {
314
						self.controller._selectionMgr.setFocused();
315
					})
316
					.on('dragover','.egwGridView_grid tr',false).attr("dropzone","copy")
317
318
					.on('drop', '.egwGridView_grid tr',function(e) {
319
						self.handle_drop(e,this);
320
						return false;
321
					});
322
			}
323
		}
324
		// stop invalidation in no visible tabs
325
		jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) {
326
			if(this.controller && this.controller._grid)
327
			{
328
				this.controller._grid.doInvalidate = false;
329
			}
330
		},this));
331
		jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) {
332
			if(this.controller && this.controller._grid)
333
			{
334
				this.controller._grid.doInvalidate = true;
335
			}
336
		},this));
337
338
		return true;
339
	},
340
341
	/**
342
	 * Implements the et2_IResizeable interface - lets the dynheight manager
343
	 * update the width and height and then update the dataview container.
344
	 */
345
	resize: function()
346
	{
347
		if (this.dynheight)
348
		{
349
			this.dynheight.update(function(_w, _h) {
350
				this.dataview.resize(_w, _h);
351
			}, this);
352
		}
353
	},
354
355
	/**
356
	 * Sorts the nextmatch widget by the given ID.
357
	 *
358
	 * @param {string} _id is the id of the data entry which should be sorted.
359
	 * @param {boolean} _asc if true, the elements are sorted ascending, otherwise
360
	 * 	descending. If not set, the sort direction will be determined
361
	 * 	automatically.
362
	 * @param {boolean} _update true/undefined: call applyFilters, false: only set sort
363
	 */
364
	sortBy: function(_id, _asc, _update) {
365
		if (typeof _update == "undefined")
366
		{
367
			_update = true;
368
		}
369
370
		// Create the "sort" entry in the active filters if it did not exist
371
		// yet.
372
		if (typeof this.activeFilters["sort"] == "undefined")
373
		{
374
			this.activeFilters["sort"] = {
375
				"id": null,
376
				"asc": true
377
			};
378
		}
379
380
		// Determine the sort direction automatically if it is not set
381
		if (typeof _asc == "undefined")
382
		{
383
			_asc = true;
384
			if (this.activeFilters["sort"].id == _id)
385
			{
386
				_asc = !this.activeFilters["sort"].asc;
387
			}
388
		}
389
390
		// Set the sortmode display
391
		this.iterateOver(function(_widget) {
392
			_widget.setSortmode((_widget.id == _id) ? (_asc ? "asc": "desc") : "none");
393
		}, this, et2_INextmatchSortable);
394
395
		if (_update)
396
		{
397
			this.applyFilters({sort: { id: _id, asc: _asc}});
398
		}
399
		else
400
		{
401
			// Update the entry in the activeFilters object
402
			this.activeFilters["sort"] = {
403
				"id": _id,
404
				"asc": _asc
405
			};
406
		}
407
	},
408
409
	/**
410
	 * Removes the sort entry from the active filters object and thus returns to
411
	 * the natural sort order.
412
	 */
413
	resetSort: function() {
414
		// Check whether the nextmatch widget is currently sorted
415
		if (typeof this.activeFilters["sort"] != "undefined")
416
		{
417
			// Reset the sortmode
418
			this.iterateOver(function(_widget) {
419
				_widget.setSortmode("none");
420
			}, this, et2_INextmatchSortable);
421
422
			// Delete the "sort" filter entry
423
			this.applyFilters({sort: undefined});
424
		}
425
	},
426
427
	/**
428
	 * Apply current or modified filters on NM widget (updating rows accordingly)
429
	 *
430
	 * @param _set filter(s) to set eg. { filter: '' } to reset filter in NM header
431
	 */
432
	applyFilters: function(_set) {
433
		var changed = false;
434
		var keep_selection = false;
435
436
		// Avoid loops cause by change events
437
		if(this.update_in_progress) return;
438
		this.update_in_progress = true;
439
440
		// Cleared explicitly
441
		if(typeof _set != 'undefined' && jQuery.isEmptyObject(_set))
442
		{
443
			changed = true;
444
			this.activeFilters = {};
445
		}
446
		if(typeof this.activeFilters == "undefined")
447
		{
448
			this.activeFilters = {col_filter: {}};
449
		}
450
		if(typeof this.activeFilters.col_filter == "undefined")
451
		{
452
			this.activeFilters.col_filter = {};
453
		}
454
455
		if (typeof _set == 'object')
456
		{
457
			for(var s in _set)
458
			{
459
				if (s == 'col_filter')
460
				{
461
					// allow apps setState() to reset all col_filter by using undefined or null for it
462
					// they can not pass {} for _set / state.state, if they need to set something
463
					if (_set.col_filter === undefined || _set.col_filter === null)
464
					{
465
						this.activeFilters.col_filter = {};
466
						changed = true;
467
					}
468
					else
469
					{
470
						for(var c in _set.col_filter)
471
						{
472
							if (this.activeFilters.col_filter[c] !== _set.col_filter[c])
473
							{
474
								if (_set.col_filter[c])
475
								{
476
									this.activeFilters.col_filter[c] = _set.col_filter[c];
477
								}
478
								else
479
								{
480
									delete this.activeFilters.col_filter[c];
481
								}
482
								changed = true;
483
							}
484
						}
485
					}
486
				}
487
				else if (s === 'selected')
488
				{
489
					changed = true;
490
					keep_selection = true;
491
					this.controller._selectionMgr.resetSelection();
492
					for(var i in _set.selected)
493
					{
494
						this.controller._selectionMgr.setSelected(_set.selected[i].indexOf('::') > 0 ? _set.selected[i] : this.controller.dataStorePrefix + '::'+_set.selected[i],true);
495
					}
496
					delete _set.selected;
497
				}
498
				else if (this.activeFilters[s] !== _set[s])
499
				{
500
					this.activeFilters[s] = _set[s];
501
					changed = true;
502
				}
503
			}
504
		}
505
506
		this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters);
507
508
		// Keep the selection after applying filters, but only if unchanged
509
		if(!changed || keep_selection)
510
		{
511
			this.controller.keepSelection();
512
		}
513
		else
514
		{
515
			// Do not keep selection
516
            this.controller._selectionMgr.resetSelection();
517
		}
518
519
		// Update the filters in the grid controller
520
		this.controller.setFilters(this.activeFilters);
521
522
		// Update the header
523
		this.header.setFilters(this.activeFilters);
524
525
		// Update any column filters
526
		this.iterateOver(function(column) {
527
			// Skip favorites - it implements et2_INextmatchHeader, but we don't want it in the filter
528
			if(typeof column.id != "undefined" && column.id.indexOf('favorite') == 0) return;
529
530
			if(typeof column.set_value != "undefined" && column.id)
531
			{
532
				column.set_value(typeof this[column.id] == "undefined" || this[column.id] == null ? "" : this[column.id]);
533
			}
534
			if (column.id && typeof column.get_value == "function")
535
			{
536
				this[column.id] = column.get_value();
537
			}
538
		}, this.activeFilters.col_filter, et2_INextmatchHeader);
539
540
		// Trigger an update
541
		this.controller.update(true);
542
543
		if(changed)
544
		{
545
			// Highlight matching favorite in sidebox
546
			if(this.getInstanceManager().app)
547
			{
548
				var appname = this.getInstanceManager().app;
549
				if(app[appname] && app[appname].highlight_favorite)
550
				{
551
					app[appname].highlight_favorite();
552
				}
553
			}
554
		}
555
556
		this.update_in_progress = false;
557
	},
558
559
	/**
560
	 * Refresh given rows for specified change
561
	 *
562
	 * Change type parameters allows for quicker refresh then complete server side reload:
563
	 * - update: request just modified data from given rows.  Sorting is not considered,
564
	 *		so if the sort field is changed, the row will not be moved.
565
	 * - edit: rows changed, but sorting may be affected.  Requires full reload.
566
	 * - delete: just delete the given rows clientside (no server interaction neccessary)
567
	 * - add: requires full reload
568
	 *
569
	 * @param {string[]|string} _row_ids rows to refresh
570
	 * @param {?string} _type "update", "edit", "delete" or "add"
571
	 *
572
	 * @see jsapi.egw_refresh()
573
	 * @fires refresh from the widget itself
574
	 */
575
	refresh: function(_row_ids, _type) {
576
		// Framework trying to refresh, but nextmatch not fully initialized
577
		if(this.controller === null || !this.div)
578
		{
579
			return;
580
		}
581
		if (!this.div.is(':visible'))	// run refresh, once we become visible again
582
		{
583
			jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch',
584
				// Important to use anonymous function instead of just 'this.refresh' because
585
				// of the parameters passed
586
				jQuery.proxy(function() {this.refresh();},this)
587
			);
588
			return;
589
		}
590
		if (typeof _type == 'undefined') _type = 'edit';
591
		if (typeof _row_ids == 'string' || typeof _row_ids == 'number') _row_ids = [_row_ids];
592
		if (typeof _row_ids == "undefined" || _row_ids === null)
593
		{
594
			this.applyFilters();
595
596
			// Trigger an event so app code can act on it
597
			jQuery(this).triggerHandler("refresh",[this]);
598
599
			return;
600
		}
601
602
		if(_type == "delete")
603
		{
604
			// Record current & next index
605
			var uid = _row_ids[0].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[0] : this.controller.dataStorePrefix + "::" + _row_ids[0];
606
			var entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid);
607
			var next = (entry.ao?entry.ao.getNext(_row_ids.length):null);
608
			if(next == null || !next.id || next.id == uid)
609
			{
610
				// No next, select previous
611
				next = (entry.ao?entry.ao.getPrevious(1):null);
612
			}
613
614
			// Stop automatic updating
615
			this.dataview.grid.doInvalidate = false;
616
			for(var i = 0; i < _row_ids.length; i++)
617
			{
618
				uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i];
619
620
				// Delete from internal references
621
				this.controller.deleteRow(uid);
622
			}
623
624
			// Select & focus next row
625
			if(next && next.id)
626
			{
627
				this.controller._selectionMgr.setSelected(next.id,true);
628
				this.controller._selectionMgr.setFocused(next.id,true);
629
			}
630
631
			// Update the count
632
			var total = this.dataview.grid._total - _row_ids.length;
633
			// This will remove the last row!
634
			// That's OK, because grid adds one in this.controller.deleteRow()
635
			this.dataview.grid.setTotalCount(total);
636
			// Re-enable automatic updating
637
			this.dataview.grid.doInvalidate = true;
638
			this.dataview.grid.invalidate();
639
		}
640
641
		id_loop:
642
		for(var i = 0; i < _row_ids.length; i++)
643
		{
644
			var uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i];
645
			switch(_type)
646
			{
647
				case "update":
648
					if(!this.egw().dataRefreshUID(uid))
649
					{
650
						// Could not update just that row
651
						this.applyFilters();
652
						break id_loop;
653
					}
654
					break;
655
				case "delete":
656
					// Handled above, more code to execute after loop
657
					break;
658
				case "edit":
659
				case "add":
660
				default:
661
					// Trigger refresh
662
					this.applyFilters();
663
					break id_loop;
664
			}
665
		}
666
		// Trigger an event so app code can act on it
667
		jQuery(this).triggerHandler("refresh",[this,_row_ids,_type]);
668
	},
669
670
	/**
671
	 * Gets the selection
672
	 *
673
	 * @return Object { ids: [UIDs], inverted: boolean}
674
	 */
675
	getSelection: function() {
676
		var selected = this.controller && this.controller._selectionMgr ? this.controller._selectionMgr.getSelected() : null;
677
		if(typeof selected == "object" && selected != null)
678
		{
679
			return selected;
680
		}
681
		return {ids:[],all:false};
682
	},
683
684
	/**
685
	 * Event handler for when the selection changes
686
	 *
687
	 * If the onselect attribute was set to a string with javascript code, it will
688
	 * be executed "legacy style".  You can get the selected values with getSelection().
689
	 * If the onselect attribute is in app.appname.function style, it will be called
690
	 * with the nextmatch and an array of selected row IDs.
691
	 *
692
	 * The array can be empty, if user cleared the selection.
693
	 *
694
	 * @param action ActionObject From action system.  Ignored.
695
	 * @param senders ActionObjectImplemetation From action system.  Ignored.
696
	 */
697
	onselect: function(action,senders) {
698
		// Execute the JS code connected to the event handler
699
		if (typeof this.options.onselect == 'function')
700
		{
701
			return this.options.onselect.call(this, this.getSelection().ids, this);
702
		}
703
	},
704
705
	/**
706
	 * Generates the column caption for the given column widget
707
	 *
708
	 * @param {et2_widget} _widget
709
	 */
710
	_genColumnCaption: function(_widget) {
711
		var result = null;
712
713
		if(typeof _widget._genColumnCaption == "function") return _widget._genColumnCaption();
714
715
		_widget.iterateOver(function(_widget) {
716
			var label = (_widget.options.label ? _widget.options.label : _widget.options.empty_label);
717
			if (!label) return;	// skip empty, undefined or null labels
718
			if (!result)
719
			{
720
				result = label;
721
			}
722
			else
723
			{
724
				result += ", " + label;
725
			}
726
		}, this, et2_INextmatchHeader);
727
728
		return result;
729
	},
730
731
	/**
732
	 * Generates the column name (internal) for the given column widget
733
	 * Used in preferences to refer to the columns by name instead of position
734
	 *
735
	 * See _getColumnCaption() for human fiendly captions
736
	 *
737
	 * @param {et2_widget} _widget
738
	 */
739
	_getColumnName: function(_widget) {
740
		if(typeof _widget._getColumnName == 'function') return _widget._getColumnName();
741
742
		var name = _widget.id;
743
		var child_names = [];
744
		var children = _widget.getChildren();
745
		for(var i = 0; i < children.length; i++) {
746
			if(children[i].id) child_names.push(children[i].id);
747
		}
748
749
		var colName =  name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_");
750
		if(colName == "") {
751
			this.egw().debug("info", "Unable to generate nm column name for ", _widget);
752
		}
753
		return colName;
754
	},
755
756
757
	/**
758
	 * Retrieve the user's preferences for this nextmatch merged with defaults
759
	 * Column display, column size, etc.
760
	 */
761
	_getPreferences: function() {
762
		// Read preference or default for column visibility
763
		var negated = false;
764
		var columnPreference = "";
765
		if(this.options.settings.default_cols)
766
		{
767
			negated = this.options.settings.default_cols[0] == "!";
768
			columnPreference = negated ? this.options.settings.default_cols.substring(1) : this.options.settings.default_cols;
769
		}
770
		if(this.options.settings.selectcols)
771
		{
772
			columnPreference = this.options.settings.selectcols;
773
			negated = false;
774
		}
775
		if(!this.options.settings.columnselection_pref)
776
		{
777
			// Set preference name so changes are saved
778
			this.options.settings.columnselection_pref = this.options.template;
779
		}
780
781
		var app = '';
782
		if(this.options.settings.columnselection_pref) {
783
			var pref = {};
784
			var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, ".");
785
			if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0)
786
			{
787
				app = list[0].substring('nextmatch'.length+1);
788
				pref = egw.preference(this.options.settings.columnselection_pref, app);
789
			}
790
			else
791
			{
792
				app = list[0];
793
				// 'nextmatch-' prefix is there in preference name, but not in setting, so add it in
794
				pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app);
795
			}
796
			if(pref)
797
			{
798
				negated = (pref[0] == "!");
799
				columnPreference = negated ? pref.substring(1) : pref;
800
			}
801
		}
802
803
		// If no column preference or default set, use all columns
804
		if(typeof columnPreference =="string" && columnPreference.length == 0)
805
		{
806
			columnDisplay = {};
807
			negated = true;
808
		}
809
810
		var columnDisplay = typeof columnPreference === "string"
811
				? et2_csvSplit(columnPreference,null,",") : columnPreference;
812
813
		// Adjusted column sizes
814
		var size = {};
815
		if(this.options.settings.columnselection_pref && app)
816
		{
817
			var size_pref = this.options.settings.columnselection_pref +"-size";
818
819
			// If columnselection pref is missing prefix, add it in
820
			if(size_pref.indexOf('nextmatch') == -1)
821
			{
822
				size_pref = 'nextmatch-'+size_pref;
823
			}
824
			size = this.egw().preference(size_pref, app);
825
		}
826
		if(!size) size = {};
827
		return {
828
			visible: columnDisplay,
829
			visible_negated: negated,
830
			size: size
831
		};
832
	},
833
834
	/**
835
	 * Apply stored user preferences to discovered columns
836
	 *
837
	 * @param {array} _row
838
	 * @param {array} _colData
839
	 */
840
	_applyUserPreferences: function(_row, _colData) {
841
		var prefs = this._getPreferences();
842
		var columnDisplay = prefs.visible;
843
		var size = prefs.size;
844
		var negated = prefs.visible_negated;
845
		var colName = '';
846
847
		// Add in display preferences
848
		if(columnDisplay && columnDisplay.length > 0)
849
		{
850
			RowLoop:
851
			for(var i = 0; i < _row.length; i++)
852
			{
853
				colName = '';
854
				if(_row[i].disabled === true)
855
				{
856
					_colData[i].visible = false;
857
					continue;
858
				}
859
860
				// Customfields needs special processing
861
				if(_row[i].widget.instanceOf(et2_nextmatch_customfields))
862
				{
863
					// Find cf field
864
					for(var j = 0; j < columnDisplay.length; j++)
865
					{
866
						if(columnDisplay[j].indexOf(_row[i].widget.id) == 0) {
867
							_row[i].widget.options.fields = {};
868
							for(var k = j; k < columnDisplay.length; k++)
869
							{
870
								if(columnDisplay[k].indexOf(_row[i].widget.prefix) == 0)
871
								{
872
									_row[i].widget.options.fields[columnDisplay[k].substr(1)] = true;
873
								}
874
							}
875
							// Resets field visibility too
876
							_row[i].widget._getColumnName();
877
							_colData[i].visible = !(negated || jQuery.isEmptyObject(_row[i].widget.options.fields));
878
							break;
879
						}
880
					}
881
					// Disable if there are no custom fields
882
					if(jQuery.isEmptyObject(_row[i].widget.customfields))
883
					{
884
						_colData[i].visible = false;
885
						continue;
886
					}
887
					colName = _row[i].widget.id;
888
				}
889
				else
890
				{
891
					colName = this._getColumnName(_row[i].widget);
892
				}
893
				if(!colName) continue;
894
895
				if(size[colName])
896
				{
897
					// Make sure percentages stay percentages, and forget any preference otherwise
898
					if(_colData[i].width.charAt(_colData[i].width.length - 1) == "%")
899
					{
900
						_colData[i].width = typeof size[colName] == 'string' && size[colName].charAt(size[colName].length - 1) == "%" ? size[colName] : _colData[i].width;
901
					}
902
					else
903
					{
904
						_colData[i].width = parseInt(size[colName])+'px';
905
					}
906
				}
907
				for(var j = 0; j < columnDisplay.length; j++)
908
				{
909
					if(columnDisplay[j] == colName)
910
					{
911
						_colData[i].visible = !negated;
912
913
						continue RowLoop;
914
					}
915
				}
916
				_colData[i].visible = negated;
917
			}
918
		}
919
	},
920
921
	/**
922
	 * Take current column display settings and store them in this.egw().preferences
923
	 * for next time
924
	 */
925
	_updateUserPreferences: function() {
926
		var colMgr = this.dataview.getColumnMgr();
927
		var app = "";
928
		if(!this.options.settings.columnselection_pref) {
929
			this.options.settings.columnselection_pref = this.options.template;
930
		}
931
932
		var visibility = colMgr.getColumnVisibilitySet();
933
		var colDisplay = [];
934
		var colSize = {};
935
		var custom_fields = [];
936
937
		// visibility is indexed by internal ID, widget is referenced by position, preference needs name
938
		for(var i = 0; i < colMgr.columns.length; i++)
939
		{
940
			var widget = this.columns[i].widget;
941
			var colName = this._getColumnName(widget);
942
			if(colName) {
943
				// Server side wants each cf listed as a seperate column
944
				if(widget.instanceOf(et2_nextmatch_customfields))
945
				{
946
					// Just the ID for server side, not the whole nm name - some apps use it to skip custom fields
947
					colName = widget.id;
948
					for(var name in widget.options.fields) {
949
						 if(widget.options.fields[name]) custom_fields.push(widget.prefix+name);
950
					}
951
				}
952
				if(visibility[colMgr.columns[i].id].visible) colDisplay.push(colName);
953
954
				// When saving sizes, only save columns with explicit values, preserving relative vs fixed
955
				// Others will be left to flex if width changes or more columns are added
956
				if(colMgr.columns[i].relativeWidth)
957
				{
958
					colSize[colName] = (colMgr.columns[i].relativeWidth * 100) + "%";
959
				}
960
				else if (colMgr.columns[i].fixedWidth)
961
				{
962
					colSize[colName] = colMgr.columns[i].fixedWidth;
963
				}
964
			} else if (colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) {
965
				this.egw().debug("info", "Could not save column width - no name", colMgr.columns[i].id);
966
			}
967
		}
968
969
		var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, ".");
970
		var pref = this.options.settings.columnselection_pref;
971
		if(pref.indexOf('nextmatch') == 0)
972
		{
973
			app = list[0].substring('nextmatch'.length+1);
974
		}
975
		else
976
		{
977
			app = list[0];
978
			// 'nextmatch-' prefix is there in preference name, but not in setting, so add it in
979
			pref = "nextmatch-"+this.options.settings.columnselection_pref;
980
		}
981
982
		// Server side wants each cf listed as a seperate column
983
		jQuery.merge(colDisplay, custom_fields);
984
985
		// Update query value, so data source can use visible columns to exclude expensive sub-queries
986
		var oldCols = this.activeFilters.selectcols ? this.activeFilters.selectcols : [];
987
988
		this.activeFilters.selectcols = colDisplay;
989
990
		// We don't need to re-query if they've removed a column
991
		var changed = [];
992
		ColLoop:
993
		for(var i = 0; i < colDisplay.length; i++)
994
		{
995
			for(var j = 0; j < oldCols.length; j++) {
996
				 if(colDisplay[i] == oldCols[j]) continue ColLoop;
997
			}
998
			changed.push(colDisplay[i]);
999
		}
1000
1001
		// If a custom field column was added, throw away cache to deal with
1002
		// efficient apps that didn't send all custom fields in the first request
1003
		var cf_added = jQuery(changed).filter(jQuery(custom_fields)).length > 0;
1004
1005
		// Save visible columns
1006
		// 'nextmatch-' prefix is there in preference name, but not in setting, so add it in
1007
		this.egw().set_preference(app, pref, colDisplay.join(","),
1008
			// Use callback after the preference gets set to trigger refresh, in case app
1009
			// isn't looking at selectcols and just uses preference
1010
			cf_added ? jQuery.proxy(function() {if(this.controller) this.controller.update(true);}, this):null
1011
		);
1012
1013
		// Save adjusted column sizes
1014
		this.egw().set_preference(app, pref+"-size", colSize);
1015
1016
		// No significant change (just normal columns shown) and no need to wait,
1017
		// but the grid still needs to be redrawn if a custom field was removed because
1018
		// the cell content changed.  This is a cheaper refresh than the callback,
1019
		// this.controller.update(true)
1020
		if((changed.length || custom_fields.length) && !cf_added) this.applyFilters();
1021
	},
1022
1023
	_parseHeaderRow: function(_row, _colData) {
1024
1025
		// Make sure there's a widget - cols disabled in template can be missing them, and the header really likes to have a widget
1026
1027
		for (var x = 0; x < _row.length; x++)
1028
		{
1029
			if(!_row[x].widget)
1030
			{
1031
				_row[x].widget = et2_createWidget("label");
1032
			}
1033
		}
1034
1035
		// Get column display preference
1036
		this._applyUserPreferences(_row, _colData);
1037
1038
		// Go over the header row and create the column entries
1039
		this.columns = new Array(_row.length);
1040
		var columnData = new Array(_row.length);
1041
1042
		// No action columns in et2
1043
		var remove_action_index = null;
1044
1045
		for (var x = 0; x < _row.length; x++)
1046
		{
1047
			this.columns[x] = jQuery.extend({
1048
				"widget": _row[x].widget
1049
			},_colData[x]);
1050
1051
			var visibility = (!_colData[x] || _colData[x].visible) ?
1052
				ET2_COL_VISIBILITY_VISIBLE :
1053
				ET2_COL_VISIBILITY_INVISIBLE;
1054
			if(_colData[x].disabled && _colData[x].disabled !=='' &&
1055
				this.getArrayMgr("content").parseBoolExpression(_colData[x].disabled))
1056
			{
1057
				visibility = ET2_COL_VISIBILITY_DISABLED;
1058
			}
1059
			columnData[x] = {
1060
				"id": "col_" + x,
1061
				"caption": this._genColumnCaption(_row[x].widget),
1062
				"visibility": visibility,
1063
				"width": _colData[x] ? _colData[x].width : 0
1064
			};
1065
			if(_colData[x].width === 'auto')
1066
			{
1067
				// Column manager does not understand 'auto', which grid widget
1068
				// uses if width is not set
1069
				columnData[x].width = '100%';
1070
			}
1071
			if(_colData[x].minWidth)
1072
			{
1073
				columnData[x].minWidth = _colData[x].minWidth;
1074
			}
1075
			if(_colData[x].maxWidth)
1076
			{
1077
				columnData[x].maxWidth = _colData[x].maxWidth;
1078
			}
1079
1080
			// No action columns in et2
1081
			var colName = this._getColumnName(_row[x].widget);
1082
			if(colName == 'actions' || colName == 'legacy_actions' || colName == 'legacy_actions_check_all')
1083
			{
1084
				remove_action_index = x;
1085
				continue;
0 ignored issues
show
Unused Code introduced by
This continue has no effect on the loop flow and can be removed.
Loading history...
1086
			}
1087
			else if (!colName)
1088
			{
1089
				// Unnamed column cannot be toggled or saved
1090
				columnData[x].visibility = ET2_COL_VISIBILITY_ALWAYS_NOSELECT;
1091
			}
1092
1093
		}
1094
1095
		// Remove action column
1096
		if(remove_action_index != null)
1097
		{
1098
			this.columns.splice(remove_action_index,remove_action_index);
1099
			columnData.splice(remove_action_index,remove_action_index);
1100
			_colData.splice(remove_action_index,remove_action_index);
1101
		}
1102
1103
		// Create the column manager and update the grid container
1104
		this.dataview.setColumns(columnData);
1105
1106
		for (var x = 0; x < _row.length; x++)
1107
		{
1108
			// Append the widget to this container
1109
			this.addChild(_row[x].widget);
1110
		}
1111
1112
		// Create the nextmatch row provider
1113
		this.rowProvider = new et2_nextmatch_rowProvider(
1114
			this.dataview.rowProvider, this._getSubgrid, this);
1115
1116
		// Register handler to update preferences when column properties are changed
1117
		var self = this;
1118
		this.dataview.onUpdateColumns = function() {
1119
			// Use apply to make sure context is there
1120
			self._updateUserPreferences.apply(self);
1121
1122
			// Allow column widgets a chance to resize
1123
			self.iterateOver(function(widget) {widget.resize();}, self, et2_IResizeable);
1124
		};
1125
1126
		// Register handler for column selection popup, or disable
1127
		if(this.selectPopup)
1128
		{
1129
			this.selectPopup.remove();
1130
			this.selectPopup = null;
1131
		}
1132
		if(this.options.settings.no_columnselection)
1133
		{
1134
			this.dataview.selectColumnsClick = function() {return false;};
1135
			jQuery('span.selectcols',this.dataview.headTr).hide();
1136
		}
1137
		else
1138
		{
1139
			jQuery('span.selectcols',this.dataview.headTr).show();
1140
			this.dataview.selectColumnsClick = function(event) {
1141
				self._selectColumnsClick(event);
1142
			};
1143
		}
1144
	},
1145
1146
	_parseDataRow: function(_row, _rowData, _colData) {
1147
		var columnWidgets = new Array(this.columns.length);
1148
1149
		for (var x = 0; x < columnWidgets.length; x++)
1150
		{
1151
			if (typeof _row[x] != "undefined" && _row[x].widget)
1152
			{
1153
				columnWidgets[x] = _row[x].widget;
1154
1155
				// Append the widget to this container
1156
				this.addChild(_row[x].widget);
1157
			}
1158
			else
1159
			{
1160
				columnWidgets[x] = _row[x].widget;
1161
			}
1162
			// Pass along column alignment
1163
			if(_row[x].align && columnWidgets[x])
1164
			{
1165
				columnWidgets[x].align = _row[x].align;
1166
			}
1167
		}
1168
1169
		this.rowProvider.setDataRowTemplate(columnWidgets, _rowData, this);
1170
1171
1172
		// Create the grid controller
1173
		this.controller = new et2_nextmatch_controller(
1174
			null,
1175
			this.egw(),
1176
			this.getInstanceManager().etemplate_exec_id,
1177
			this,
1178
			null,
1179
			this.dataview.grid,
1180
			this.rowProvider,
1181
			this.options.settings.action_links,
1182
			null,
1183
			this.options.actions
1184
		);
1185
1186
		// Need to trigger empty row the first time
1187
		if(total == 0) this.controller._emptyRow();
0 ignored issues
show
Bug introduced by
The variable total seems to be never initialized.
Loading history...
1188
1189
		// Set data cache prefix to either provided custom or auto
1190
		if(!this.options.settings.dataStorePrefix)
1191
		{
1192
			// Use jsapi data module to update
1193
			var list = this.options.settings.get_rows.split('.', 2);
1194
			if (list.length < 2) list = this.options.settings.get_rows.split('_');	// support "app_something::method"
1195
			this.options.settings.dataStorePrefix = list[0];
1196
		}
1197
		this.controller.setPrefix(this.options.settings.dataStorePrefix);
1198
1199
		// Set the view
1200
		this.controller._view = this.view;
1201
1202
		// Load the initial order
1203
		/*this.controller.loadInitialOrder(this._getInitialOrder(
1204
			this.options.settings.rows, this.options.settings.row_id
1205
		));*/
1206
1207
		// Set the initial row count
1208
		var total = typeof this.options.settings.total != "undefined" ?
1209
			this.options.settings.total : 0;
1210
		// This triggers an invalidate, which updates the grid
1211
		this.dataview.grid.setTotalCount(total);
1212
1213
		// Insert any data sent from server, so invalidate finds data already
1214
		if(this.options.settings.rows && this.options.settings.num_rows)
1215
		{
1216
			this.controller.loadInitialData(
1217
				this.options.settings.dataStorePrefix,
1218
				this.options.settings.row_id,
1219
				this.options.settings.rows
1220
			);
1221
			// Remove, to prevent duplication
1222
			delete this.options.settings.rows;
1223
		}
1224
	},
1225
1226
	_parseGrid: function(_grid) {
1227
		// Search the rows for a header-row - if one is found, parse it
1228
		for (var y = 0; y < _grid.rowData.length; y++)
1229
		{
1230
			// Parse the first row as a header, need header to parse the data rows
1231
			if (_grid.rowData[y]["class"] == "th" || y == 0)
1232
			{
1233
				this._parseHeaderRow(_grid.cells[y], _grid.colData);
1234
			}
1235
			else
1236
			{
1237
				this._parseDataRow(_grid.cells[y], _grid.rowData[y],
1238
						_grid.colData);
1239
			}
1240
		}
1241
		this.dataview.table.resize();
1242
	},
1243
1244
	_getSubgrid: function (_row, _data, _controller) {
1245
		// Fetch the id of the element described by _data, this will be the
1246
		// parent_id of the elements in the subgrid
1247
		var rowId = _data.content[this.options.settings.row_id];
1248
1249
		// Create a new grid with the row as parent and the dataview grid as
1250
		// parent grid
1251
		var grid = new et2_dataview_grid(_row, this.dataview.grid);
1252
1253
		// Create a new controller for the grid
1254
		var controller = new et2_nextmatch_controller(
1255
				_controller,
1256
				this.egw(),
1257
				this.getInstanceManager().etemplate_exec_id,
1258
				this,
1259
				rowId,
1260
				grid,
1261
				this.rowProvider,
1262
				this.options.settings.action_links,
1263
				_controller.getObjectManager()
1264
		);
1265
		controller.update();
1266
1267
		// Register inside the destruction callback of the grid
1268
		grid.setDestroyCallback(function () {
1269
			controller.free();
1270
		});
1271
1272
		return grid;
1273
	},
1274
1275
	_getInitialOrder: function (_rows, _rowId) {
1276
1277
		var _order = [];
1278
1279
		// Get the length of the non-numerical rows arra
1280
		var len = 0;
1281
		for (var key in _rows) {
1282
			if (!isNaN(key) && parseInt(key) > len)
1283
				len = parseInt(key);
1284
		}
1285
1286
		// Iterate over the rows
1287
		for (var i = 0; i < len; i++)
1288
		{
1289
			// Get the uid from the data
1290
			var uid = this.egw().appName + '::' + _rows[i][_rowId];
1291
1292
			// Store the data for that uid
1293
			this.egw().dataStoreUID(uid, _rows[i]);
1294
1295
			// Push the uid onto the order array
1296
			_order.push(uid);
1297
		}
1298
1299
		return _order;
1300
	},
1301
1302
	_selectColumnsClick: function(e) {
1303
		var self = this;
1304
		var columnMgr = this.dataview.getColumnMgr();
1305
1306
		// ID for faking letter selection in column selection
1307
		var LETTERS = '~search_letter~';
1308
1309
		var columns = {};
1310
		var columns_selected = [];
1311
1312
		for (var i = 0; i < columnMgr.columns.length; i++)
1313
		{
1314
			var col = columnMgr.columns[i];
1315
			var widget = this.columns[i].widget;
1316
1317
			if(col.caption && col.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT &&
1318
				col.visibility !== ET2_COL_VISIBILITY_DISABLED)
1319
			{
1320
				columns[col.id] = col.caption;
1321
				if(col.visibility == ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(col.id);
1322
			}
1323
			// Custom fields get listed separately
1324
			if(widget.instanceOf(et2_nextmatch_customfields))
1325
			{
1326
				if(jQuery.isEmptyObject(widget.customfields))
1327
				{
1328
					// No customfields defined, don't show column
1329
					delete(columns[col.id]);
1330
					continue;
1331
				}
1332
				for(var field_name in widget.customfields)
1333
				{
1334
					columns[widget.prefix+field_name] = " - "+widget.customfields[field_name].label;
1335
					if(widget.options.fields[field_name]) columns_selected.push(et2_customfields_list.prototype.prefix+field_name);
1336
				}
1337
			}
1338
		}
1339
1340
		// Letter search
1341
		if(this.options.settings.lettersearch)
1342
		{
1343
			columns[LETTERS] = egw.lang('Search letter');
1344
			if(this.header.lettersearch.is(':visible')) columns_selected.push(LETTERS);
1345
		}
1346
1347
		// Build the popup
1348
		if(!this.selectPopup)
1349
		{
1350
			var select = et2_createWidget("select", {
1351
				multiple: true,
1352
				rows: 8,
1353
				empty_label:this.egw().lang("select columns"),
1354
				selected_first: false
1355
			}, this);
1356
			select.set_select_options(columns);
1357
			select.set_value(columns_selected);
1358
1359
			var autoRefresh = et2_createWidget("select", {
1360
				"empty_label":"Refresh"
1361
			}, this);
1362
			autoRefresh.set_id("nm_autorefresh");
1363
			autoRefresh.set_select_options({
1364
				// Cause [unknown] problems with mail
1365
				//30: "30 seconds",
1366
				//60: "1 Minute",
1367
				300: "5 Minutes",
1368
				900: "15 Minutes",
1369
				1800: "30 Minutes"
1370
			});
1371
			autoRefresh.set_value(this._get_autorefresh());
1372
			autoRefresh.set_statustext(egw.lang("Automatically refresh list"));
1373
1374
			var defaultCheck = et2_createWidget("select", {"empty_label":"Preference"}, this);
1375
			defaultCheck.set_id('nm_col_preference');
1376
			defaultCheck.set_select_options({
1377
				'default': {label: 'Default',title:'Set these columns as the default'},
1378
				'reset':   {label: 'Reset', title:"Reset all user's column preferences"},
1379
				'force':   {label: 'Force', title:'Force column preference so users cannot change it'}
1380
			});
1381
			defaultCheck.set_value(this.options.settings.columns_forced ? 'force': '');
1382
1383
			var okButton = et2_createWidget("buttononly", {}, this);
1384
			okButton.set_label(this.egw().lang("ok"));
1385
			okButton.onclick = function() {
1386
				// Update visibility
1387
				var visibility = {};
1388
				for (var i = 0; i < columnMgr.columns.length; i++)
1389
				{
1390
					var col = columnMgr.columns[i];
1391
					if(col.caption && col.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT &&
1392
						col.visibility !== ET2_COL_VISIBILITY_DISABLED )
1393
					{
1394
						visibility[col.id] = {visible: false};
1395
					}
1396
				}
1397
				var value = select.getValue();
1398
1399
				// Update & remove letter filter
1400
				if(self.header.lettersearch)
1401
				{
1402
					var show_letters = true;
1403
					if(value.indexOf(LETTERS) >= 0)
1404
					{
1405
						value.splice(value.indexOf(LETTERS),1);
1406
					}
1407
					else
1408
					{
1409
						show_letters = false;
1410
					}
1411
					self._set_lettersearch(show_letters);
1412
				}
1413
1414
				var column = 0;
1415
				for(var i = 0; i < value.length; i++)
1416
				{
1417
					// Handle skipped columns
1418
					while(value[i] != "col_"+column && column < columnMgr.columns.length)
1419
					{
1420
						column++;
1421
					}
1422
					if(visibility[value[i]])
1423
					{
1424
						visibility[value[i]].visible = true;
1425
					}
1426
					// Custom fields are listed seperately in column list, but are only 1 column
1427
					if(self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) {
1428
						var cf = self.columns[column].widget.options.customfields;
1429
						var visible = self.columns[column].widget.options.fields;
1430
1431
						// Turn off all custom fields
1432
						for(var field_name in cf)
1433
						{
1434
							visible[field_name] = false;
1435
						}
1436
						// Turn on selected custom fields - start from 0 in case they're not in order
1437
						for(var j = 0; j < value.length; j++)
1438
						{
1439
							if(value[j].indexOf(et2_customfields_list.prototype.prefix) != 0) continue;
1440
							visible[value[j].substring(1)] = true;
1441
							i++;
1442
						}
1443
						self.columns[column].widget.set_visible(visible);
1444
					}
1445
				}
1446
				columnMgr.setColumnVisibilitySet(visibility);
1447
1448
				// Hide popup
1449
				self.selectPopup.toggle();
1450
1451
				self.dataview.updateColumns();
1452
1453
				// Auto refresh
1454
				self._set_autorefresh(autoRefresh.get_value());
1455
1456
				// Set default or clear forced
1457
				if(show_letters)
1458
				{
1459
					self.activeFilters.selectcols.push('lettersearch');
1460
				}
1461
				self.getInstanceManager().submit();
1462
1463
				self.selectPopup = null;
1464
			};
1465
1466
			var cancelButton = et2_createWidget("buttononly", {}, this);
1467
			cancelButton.set_label(this.egw().lang("cancel"));
1468
			cancelButton.onclick = function() {
1469
				self.selectPopup.toggle();
1470
				self.selectPopup = null;
1471
			};
1472
1473
			this.selectPopup = jQuery(document.createElement("div"))
1474
				.addClass("colselection ui-dialog ui-widget-content")
1475
				.append(select.getDOMNode())
1476
				.append(okButton.getDOMNode())
1477
				.append(cancelButton.getDOMNode())
1478
				.appendTo(this.innerDiv);
1479
1480
			// Add autorefresh
1481
			this.selectPopup.append(autoRefresh.getSurroundings().getDOMNode(autoRefresh.getDOMNode()));
1482
1483
			// Add default checkbox for admins
1484
			var apps = this.egw().user('apps');
1485
			if(apps['admin'])
1486
			{
1487
				this.selectPopup.append(defaultCheck.getSurroundings().getDOMNode(defaultCheck.getDOMNode()));
1488
			}
1489
		}
1490
		else
1491
		{
1492
			this.selectPopup.toggle();
1493
		}
1494
		var t_position = jQuery(e.target).position();
1495
		var s_position = this.div.position();
1496
		this.selectPopup.css("top", t_position.top)
1497
			.css("left", s_position.left + this.div.width() - this.selectPopup.width());
1498
	},
1499
1500
	/**
1501
	 * Set the currently displayed columns, without updating user's preference
1502
	 *
1503
	 * @param {string[]} column_list List of column names
1504
	 * @param {boolean} trigger_update =false - explicitly trigger an update
1505
	 */
1506
	set_columns: function(column_list, trigger_update)
1507
	{
1508
		var columnMgr = this.dataview.getColumnMgr();
1509
		var visibility = {};
1510
1511
		// Initialize to false
1512
		for (var i = 0; i < columnMgr.columns.length; i++)
1513
		{
1514
			var col = columnMgr.columns[i];
1515
			if(col.caption && col.visibility != ET2_COL_VISIBILITY_ALWAYS_NOSELECT )
1516
			{
1517
				visibility[col.id] = {visible: false};
1518
			}
1519
		}
1520
		for(var i = 0; i < this.columns.length; i++)
1521
		{
1522
1523
			var widget = this.columns[i].widget;
1524
			var colName = this._getColumnName(widget);
1525
			if(column_list.indexOf(colName) !== -1 &&
1526
				typeof visibility[columnMgr.columns[i].id] !== 'undefined'
1527
			)
1528
			{
1529
				visibility[columnMgr.columns[i].id].visible = true;
1530
			}
1531
			// Custom fields are listed seperately in column list, but are only 1 column
1532
			if(widget && widget.instanceOf(et2_nextmatch_customfields)) {
1533
1534
				// Just the ID for server side, not the whole nm name - some apps use it to skip custom fields
1535
				colName = widget.id;
1536
				if(column_list.indexOf(colName) !== -1)
1537
				{
1538
					visibility[columnMgr.columns[i].id].visible = true;
1539
				}
1540
1541
				var cf = this.columns[i].widget.options.customfields;
1542
				var visible = this.columns[i].widget.options.fields;
1543
1544
				// Turn off all custom fields
1545
				for(var field_name in cf)
1546
				{
1547
					visible[field_name] = false;
1548
				}
1549
				// Turn on selected custom fields - start from 0 in case they're not in order
1550
				for(var j = 0; j < column_list.length; j++)
1551
				{
1552
					if(column_list[j].indexOf(et2_customfields_list.prototype.prefix) != 0) continue;
1553
					visible[column_list[j].substring(1)] = true;
1554
				}
1555
				widget.set_visible(visible);
1556
			}
1557
		}
1558
		columnMgr.setColumnVisibilitySet(visibility);
1559
1560
		// We don't want to update user's preference, so directly update
1561
		this.dataview._updateColumns();
1562
1563
		// Allow column widgets a chance to resize
1564
		this.iterateOver(function(widget) {widget.resize();}, this, et2_IResizeable);
1565
	},
1566
1567
	/**
1568
	 * Set the letter search preference, and update the UI
1569
	 *
1570
	 * @param {boolean} letters_on
1571
	 */
1572
	_set_lettersearch: function(letters_on) {
1573
		if(letters_on)
1574
		{
1575
			this.header.lettersearch.show();
1576
		}
1577
		else
1578
		{
1579
			this.header.lettersearch.hide();
1580
		}
1581
		var lettersearch_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-lettersearch";
1582
		this.egw().set_preference(this.egw().getAppName(),lettersearch_preference,letters_on);
1583
	},
1584
1585
	/**
1586
	 * Set the auto-refresh time period, and starts the timer if not started
1587
	 *
1588
	 * @param time int Refresh period, in seconds
1589
	 */
1590
	_set_autorefresh: function(time) {
1591
		// Store preference
1592
		var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh";
1593
		var app = this.options.template.split(".");
1594
		if(this._get_autorefresh() != time)
1595
		{
1596
			this.egw().set_preference(app[0],refresh_preference,time);
1597
		}
1598
1599
		// Start / update timer
1600
		if (this._autorefresh_timer)
1601
		{
1602
			window.clearInterval(this._autorefresh_timer);
1603
			delete this._autorefresh_timer;
1604
		}
1605
		if(time > 0)
1606
		{
1607
			this._autorefresh_timer = setInterval(jQuery.proxy(this.controller.update, this.controller), time * 1000);
1608
1609
			// Bind to tab show/hide events, so that we don't bother refreshing in the background
1610
			jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) {
1611
				// Stop
1612
				window.clearInterval(this._autorefresh_timer);
1613
				jQuery(e.target).off(e);
1614
1615
				// If the autorefresh time is up, bind once to trigger a refresh
1616
				// (if needed) when tab is activated again
1617
				this._autorefresh_timer = setTimeout(jQuery.proxy(function() {
1618
					// Check in case it was stopped / destroyed since
1619
					if(!this._autorefresh_timer || !this.getInstanceManager()) return;
1620
1621
					jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch',
1622
						// Important to use anonymous function instead of just 'this.refresh' because
1623
						// of the parameters passed
1624
						jQuery.proxy(function() {this.refresh();},this)
1625
					);
1626
				},this), time*1000);
1627
			},this));
1628
			jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) {
1629
				// Start normal autorefresh timer again
1630
				this._set_autorefresh(this._get_autorefresh());
1631
				jQuery(e.target).off(e);
1632
			},this));
1633
		}
1634
	},
1635
1636
	/**
1637
	 * Get the auto-refresh timer
1638
	 *
1639
	 * @return int Refresh period, in secods
1640
	 */
1641
	_get_autorefresh: function() {
1642
		var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh";
1643
		var app = this.options.template.split(".");
1644
		return this.egw().preference(refresh_preference,app[0]);
1645
	},
1646
1647
	/**
1648
	 * When the template attribute is set, the nextmatch widget tries to load
1649
	 * that template and to fetch the grid which is inside of it. It then calls
1650
	 *
1651
	 * @param {string} _value template name
1652
	 */
1653
	set_template: function(_value) {
1654
		if(this.template)
1655
		{
1656
			// Stop early to prevent unneeded processing, and prevent infinite
1657
			// loops if the server changes the template in get_rows
1658
			if(this.template == _value)
1659
			{
1660
				return;
1661
			}
1662
1663
			// Free the grid components - they'll be re-created as the template is processed
1664
			this.dataview.free();
1665
			this.rowProvider.free();
1666
			this.controller.free();
1667
1668
			// Free any children from previous template
1669
			// They may get left behind because of how detached nodes are processed
1670
			// We don't use iterateOver because it checks sub-children
1671
			for(var i = this._children.length-1; i >=0 ; i--)
1672
			{
1673
				var _node = this._children[i];
1674
				if(_node != this.header) {
1675
					this.removeChild(_node);
1676
					_node.destroy();
1677
				}
1678
			}
1679
1680
			// Clear this setting if it's the same as the template, or
1681
			// the columns will not be loaded
1682
			if(this.template == this.options.settings.columnselection_pref)
1683
			{
1684
				this.options.settings.columnselection_pref = _value;
1685
			}
1686
			this.dataview = new et2_dataview(this.innerDiv, this.egw());
1687
		}
1688
1689
		// Create the template
1690
		var template = et2_createWidget("template", {"id": _value}, this);
1691
1692
		if (!template)
1693
		{
1694
			this.egw().debug("error", "Error while loading definition template for " +
1695
				"nextmatch widget.",_value);
1696
			return;
1697
		}
1698
1699
		// Deferred parse function - template might not be fully loaded
1700
		var parse = function(template)
1701
		{
1702
			// Keep the name of the template, as we'll free up the widget after parsing
1703
			this.template = _value;
1704
1705
			// Fetch the grid element and parse it
1706
			var definitionGrid = template.getChildren()[0];
1707
			if (definitionGrid && definitionGrid instanceof et2_grid)
1708
			{
1709
				this._parseGrid(definitionGrid);
1710
			}
1711
			else
1712
			{
1713
				this.egw().debug("error", "Nextmatch widget expects a grid to be the " +
1714
					"first child of the defined template.");
1715
				return;
1716
			}
1717
1718
			// Free the template again, but don't remove it
1719
			setTimeout(function() {
1720
				template.free();
1721
			},1);
1722
1723
			// Call the "setNextmatch" function of all registered
1724
			// INextmatchHeader widgets.  This updates this.activeFilters.col_filters according
1725
			// to what's in the template.
1726
			this.iterateOver(function (_node) {
1727
				_node.setNextmatch(this);
1728
			}, this, et2_INextmatchHeader);
1729
1730
			// Set filters to current values
1731
			this.controller.setFilters(this.activeFilters);
1732
1733
			// If no data was sent from the server, and num_rows is 0, the nm will be empty.
1734
			// This triggers a cache check.
1735
			if(!this.options.settings.num_rows)
1736
			{
1737
				this.controller.update();
1738
			}
1739
1740
			// Load the default sort order
1741
			if (this.options.settings.order && this.options.settings.sort)
1742
			{
1743
				this.sortBy(this.options.settings.order,
1744
					this.options.settings.sort == "ASC", false);
1745
			}
1746
1747
			// Start auto-refresh
1748
			this._set_autorefresh(this._get_autorefresh());
1749
		};
1750
1751
		// Template might not be loaded yet, defer parsing
1752
		var promise = [];
1753
		template.loadingFinished(promise);
1754
1755
		// Wait until template (& children) are done
1756
		jQuery.when.apply(null, promise).done(
1757
			jQuery.proxy(function() {
1758
				parse.call(this, template);
1759
				this.dynheight.initialized = false;
1760
				this.resize();
1761
			}, this)
1762
		);
1763
	},
1764
1765
	// Some accessors to match conventions
1766
	set_hide_header: function(hide) {
1767
		(hide ? this.header.div.hide() : this.header.div.show());
1768
	},
1769
1770
	set_header_left: function(template) {
1771
		this.header._build_header("left",template);
1772
	},
1773
	set_header_right: function(template) {
1774
		this.header._build_header("right",template);
1775
	},
1776
	set_header_row: function(template) {
1777
		this.header._build_header("row",template);
1778
	},
1779
	set_no_filter: function(bool, filter_name) {
1780
		if(typeof filter_name == 'undefined')
1781
		{
1782
			filter_name = 'filter';
1783
		}
1784
		this.options['no_'+filter_name] = bool;
1785
1786
		var filter = this.header[filter_name];
1787
		if(filter)
1788
		{
1789
			filter.set_disabled(bool);
1790
		}
1791
		else if (bool)
1792
		{
1793
			filter = this.header._build_select(filter_name, 'select',
1794
				this.settings[filter_name], this.settings[filter_name+'_no_lang']);
1795
		}
1796
	},
1797
	set_no_filter2: function(bool) {
1798
		this.set_no_filter(bool,'filter2');
1799
	},
1800
1801
	/**
1802
	 * Directly change filter value, with no server query.
1803
	 *
1804
	 * This allows the server app code to change filter value, and have it
1805
	 * updated in the client UI.
1806
	 *
1807
	 * @param {String|number} value
1808
	 */
1809
	set_filter: function(value) {
1810
		var update = this.update_in_progress;
1811
		this.update_in_progress = true;
1812
1813
		this.activeFilters.filter = value;
1814
1815
		// Update the header
1816
		this.header.setFilters(this.activeFilters);
1817
1818
		this.update_in_progress = update;
1819
	},
1820
1821
	/**
1822
	 * Directly change filter2 value, with no server query.
1823
	 *
1824
	 * This allows the server app code to change filter2 value, and have it
1825
	 * updated in the client UI.
1826
	 *
1827
	 * @param {String|number} value
1828
	 */
1829
	set_filter2: function(value) {
1830
		var update = this.update_in_progress;
1831
		this.update_in_progress = true;
1832
1833
		this.activeFilters.filter2 = value;
1834
1835
		// Update the header
1836
		this.header.setFilters(this.activeFilters);
1837
1838
		this.update_in_progress = update;
1839
	},
1840
1841
	/**
1842
	 * If nextmatch starts disabled, it will need a resize after being shown
1843
	 * to get all the sizing correct.  Override the parent to add the resize
1844
	 * when enabling.
1845
	 *
1846
	 * @param {boolean} _value
1847
	 */
1848
	set_disabled: function(_value)
1849
	{
1850
		var previous = this.disabled;
1851
		this._super.apply(this, arguments);
1852
1853
		if(previous && !_value)
1854
		{
1855
			this.resize();
1856
		}
1857
	},
1858
1859
	/**
1860
	 * Actions are handled by the controller, so ignore these during init.
1861
	 *
1862
	 * @param {object} actions
1863
	 */
1864
	set_actions: function(actions) {
1865
		if(actions != this.options.actions && this.controller != null && this.controller._actionManager)
1866
		{
1867
			for(var i = this.controller._actionManager.children.length - 1; i >= 0; i--)
1868
			{
1869
				this.controller._actionManager.children[i].remove();
1870
			}
1871
			this.options.actions = actions;
1872
			this.options.settings.action_links = this.controller._actionLinks = this._get_action_links(actions);
1873
1874
			this.controller._initActions(actions);
1875
		}
1876
	},
1877
1878
	/**
1879
	 * Switch view between row and tile.
1880
	 * This should be followed by a call to change the template to match, which
1881
	 * will cause a reload of the grid using the new settings.
1882
	 *
1883
	 * @param {string} view Either 'tile' or 'row'
1884
	 */
1885
	set_view: function(view)
1886
	{
1887
		// Restrict to the only 2 accepted values
1888
		if(view == 'tile')
1889
		{
1890
			this.view = 'tile';
1891
		}
1892
		else
1893
		{
1894
			this.view = 'row';
1895
		}
1896
	},
1897
1898
	/**
1899
	 * Set a different / additional handler for dropped files.
1900
	 *
1901
	 * File dropping doesn't work with the action system, so we handle it in the
1902
	 * nextmatch by linking automatically to the target row.  This allows an additional handler.
1903
	 * It should accept a row UID and a File[], and return a boolean Execute the default (link) action
1904
	 *
1905
	 * @param {String|Function} handler
1906
	 */
1907
	set_onfiledrop: function(handler) {
1908
		this.options.onfiledrop = handler;
1909
	},
1910
1911
	/**
1912
	 * Handle drops of files by linking to the row, if possible.
1913
	 *
1914
	 * HTML5 / native file drops conflict with jQueryUI draggable, which handles
1915
	 * all our drop actions.  So we side-step the issue by registering an additional
1916
	 * drop handler on the rows parent.  If the row/actions itself doesn't handle
1917
	 * the drop, it should bubble and get handled here.
1918
	 *
1919
	 * @param {object} event
1920
	 * @param {object} target
1921
	 */
1922
	handle_drop: function(event, target) {
1923
		// Check to see if we can handle the link
1924
		// First, find the UID
1925
		var row = this.controller.getRowByNode(target);
1926
		var uid = row.uid || null;
1927
1928
		// Get the file information
1929
		var files = [];
1930
		if(event.originalEvent && event.originalEvent.dataTransfer &&
1931
			event.originalEvent.dataTransfer.files && event.originalEvent.dataTransfer.files.length > 0)
1932
		{
1933
			files = event.originalEvent.dataTransfer.files;
1934
		}
1935
		else
1936
		{
1937
			return false;
1938
		}
1939
1940
		// Exectute the custom handler code
1941
		if (this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files))
1942
		{
1943
			return false;
1944
		}
1945
		event.stopPropagation();
1946
		event.preventDefault();
1947
1948
		if(!row || !row.uid) return false;
1949
1950
		// Link the file to the row
1951
		// just use a link widget, it's all already done
1952
		var split = uid.split('::');
1953
		var link_value = {
1954
			to_app: split.shift(),
1955
			to_id: split.join('::')
1956
		};
1957
		// Create widget and mangle to our needs
1958
		var link = et2_createWidget("link-to", {value: link_value}, this);
1959
		link.loadingFinished();
1960
		link.file_upload.set_drop_target(false);
1961
1962
		if(row.row.tr)
1963
		{
1964
			// Ignore most of the UI, just use the status indicators
1965
			var status = jQuery(document.createElement("div"))
1966
				.addClass('et2_link_to')
1967
				.width(row.row.tr.width())
1968
				.position({my: "left top", at: "left top", of: row.row.tr})
1969
				.append(link.status_span)
1970
				.append(link.file_upload.progress)
1971
				.appendTo(row.row.tr);
1972
1973
			// Bind to link event so we can remove when done
1974
			link.div.on('link.et2_link_to', function(e, linked) {
1975
				if(!linked)
1976
				{
1977
					jQuery("li.success", link.file_upload.progress)
1978
						.removeClass('success').addClass('validation_error');
1979
				}
1980
				else
1981
				{
1982
					// Update row
1983
					link._parent.refresh(uid,'edit');
1984
				}
1985
				// Fade out nicely
1986
				status.delay(linked ? 1 : 2000)
1987
					.fadeOut(500, function() {
1988
						link.free();
1989
						status.remove();
1990
					});
1991
1992
			});
1993
		}
1994
1995
		// Upload and link - this triggers the upload, which triggers the link, which triggers the cleanup and refresh
1996
		link.file_upload.set_value(files);
1997
	},
1998
1999
	getDOMNode: function(_sender) {
2000
		if (_sender == this || typeof _sender === 'undefined')
2001
		{
2002
			return this.div[0];
2003
		}
2004
		if (_sender == this.header)
2005
		{
2006
			return this.header.div[0];
2007
		}
2008
		for (var i = 0; i < this.columns.length; i++)
2009
		{
2010
			if (this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget)
2011
			{
2012
				return this.dataview.getHeaderContainerNode(i);
2013
			}
2014
		}
2015
2016
		// Let header have a chance
2017
		if(_sender && _sender._parent && _sender._parent == this)
2018
		{
2019
			return this.header.getDOMNode(_sender);
2020
		}
2021
2022
		return null;
2023
	},
2024
2025
	// Input widget
2026
2027
	/**
2028
	 * Get the current 'value' for the nextmatch
2029
	 */
2030
	getValue: function() {
2031
		var _ids = this.getSelection();
2032
2033
		// Translate the internal uids back to server uids
2034
		var idsArr = _ids.ids;
2035
		for (var i = 0; i < idsArr.length; i++)
2036
		{
2037
			idsArr[i] = idsArr[i].split("::").pop();
2038
		}
2039
		var value = {
2040
			"selected": idsArr
2041
		};
2042
		jQuery.extend(value, this.activeFilters, this.value);
2043
		return value;
2044
	},
2045
	resetDirty: function() {},
2046
	isDirty: function() { return typeof this.value !== 'undefined';},
2047
	isValid: function() { return true;},
2048
	set_value: function(_value)
2049
	{
2050
		this.value = _value;
2051
	},
2052
2053
	// Printing
2054
	/**
2055
	 * Prepare for printing
2056
	 *
2057
	 * We check for un-loaded rows, and ask the user what they want to do about them.
2058
	 * If they want to print them all, we ask the server and print when they're loaded.
2059
	 */
2060
	beforePrint: function() {
2061
		// Add the class, if needed
2062
		this.div.addClass('print');
2063
2064
		// Trigger resize, so we can fit on a page
2065
		this.dynheight.outerNode.css('max-width',this.div.css('max-width'));
2066
		this.resize();
2067
		// Reset height to auto (after width resize) so there's no restrictions
2068
		this.dynheight.innerNode.css('height', 'auto');
2069
2070
		// Check for rows that aren't loaded yet, or lots of rows
2071
		var range = this.controller._grid.getIndexRange();
2072
		this.old_height = this.controller._grid._scrollHeight;
2073
		var loaded_count = range.bottom - range.top +1;
2074
		var total = this.controller._grid.getTotalCount();
2075
2076
		// Defer the printing to ask about columns & rows
2077
		var defer = jQuery.Deferred();
2078
2079
2080
		var pref = this.options.settings.columnselection_pref;
2081
		if(pref.indexOf('nextmatch') == 0)
2082
		{
2083
			pref = 'nextmatch-'+pref;
2084
		}
2085
		var app = this.getInstanceManager().app;
2086
2087
		var columns = {};
2088
		var columnMgr = this.dataview.getColumnMgr();
2089
		pref += '_print';
2090
		var columns_selected = [];
2091
2092
		// Get column names
2093
		for (var i = 0; i < columnMgr.columns.length; i++)
2094
		{
2095
			var col = columnMgr.columns[i];
2096
			var widget = this.columns[i].widget;
2097
			var colName = this._getColumnName(widget);
2098
2099
			if(col.caption && col.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT &&
2100
				col.visibility !== ET2_COL_VISIBILITY_DISABLED)
2101
			{
2102
				columns[colName] = col.caption;
2103
				if(col.visibility === ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(colName);
2104
			}
2105
			// Custom fields get listed separately
2106
			if(widget.instanceOf(et2_nextmatch_customfields))
2107
			{
2108
				delete(columns[colName]);
2109
				colName = widget.id;
2110
				if(col.visibility === ET2_COL_VISIBILITY_VISIBLE && !
2111
					jQuery.isEmptyObject(widget.customfields)
2112
				)
2113
				{
2114
					columns[colName] = col.caption;
2115
					for(var field_name in widget.customfields)
2116
					{
2117
						columns[widget.prefix+field_name] = " - "+widget.customfields[field_name].label;
2118
						if(widget.options.fields[field_name] && columns_selected.indexOf(colName) >= 0)
2119
						{
2120
							columns_selected.push(et2_customfields_list.prototype.prefix+field_name);
2121
						}
2122
					}
2123
				}
2124
			}
2125
		}
2126
2127
		// Preference exists?  Set it now
2128
		if(this.egw().preference(pref,app))
2129
		{
2130
			this.set_columns(jQuery.extend([],this.egw().preference(pref,app)));
2131
		}
2132
2133
		var callback = jQuery.proxy(function(button, value) {
2134
			if(button === et2_dialog.CANCEL_BUTTON) {
2135
				// Give dialog a chance to close, or it will be in the print
2136
				window.setTimeout(function() {defer.reject();}, 0);
2137
				return;
2138
			}
2139
2140
			// Handle columns
2141
			this.set_columns(value.columns);
2142
			this.egw().set_preference(app,pref,value.columns);
2143
2144
			var rows = parseInt(value.row_count);
2145
			if(rows > total)
2146
			{
2147
				rows = total;
2148
			}
2149
2150
			// If they want the whole thing, style it as all
2151
			if(button === et2_dialog.OK_BUTTON && rows == this.controller._grid.getTotalCount())
2152
			{
2153
				// Add the class, gives more reliable sizing
2154
				this.div.addClass('print');
2155
				// Show it all
2156
				jQuery('.egwGridView_scrollarea',this.div).css('height','auto');
2157
			}
2158
			// We need more rows
2159
			if(button === 'dialog[all]' || rows > loaded_count)
2160
			{
2161
				var count = 0;
2162
				var fetchedCount = 0;
2163
				var cancel = false;
2164
				var nm = this;
2165
				var dialog = et2_dialog.show_dialog(
2166
					// Abort the long task if they canceled the data load
2167
					function() {count = total; cancel=true;window.setTimeout(function() {defer.reject();},0);},
2168
					egw.lang('Loading'), egw.lang('please wait...'),{},[
2169
						{"button_id": et2_dialog.CANCEL_BUTTON,"text": 'cancel',id: 'dialog[cancel]',image: 'cancel'}
2170
					]
2171
				);
2172
2173
				// dataFetch() is asyncronous, so all these requests just get fired off...
2174
				// 200 rows chosen arbitrarily to reduce requests.
2175
				do {
2176
					var ctx = {
2177
						"self": this.controller,
2178
						"start": count,
2179
						"count": Math.min(rows,200),
2180
						"lastModification": this.controller._lastModification
2181
					};
2182
					if(nm.controller.dataStorePrefix)
2183
					{
2184
						ctx.prefix = nm.controller.dataStorePrefix;
2185
					}
2186
					nm.controller.dataFetch({start:count, num_rows: Math.min(rows,200)}, function(data)  {
2187
						// Keep track
2188
						if(data && data.order)
2189
						{
2190
							fetchedCount += data.order.length;
2191
						}
2192
						nm.controller._fetchCallback.apply(this, arguments);
2193
2194
						if(fetchedCount >= rows)
0 ignored issues
show
Bug introduced by
The variable fetchedCount is changed as part of the loop loop for example by data.order.length on line 2190. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
2195
						{
2196
							if(cancel)
2197
							{
2198
								dialog.destroy();
2199
								defer.reject();
2200
								return;
2201
							}
2202
							// Use CSS to hide all but the requested rows
2203
							// Prevents us from showing more than requested, if actual height was less than average
2204
							nm.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+"+rows+"))";
2205
							egw.css(nm.print_row_selector, 'display: none');
2206
2207
							// No scrollbar in print view
2208
							jQuery('.egwGridView_scrollarea',this.div).css('overflow-y','hidden');
2209
							// Show it all
2210
							jQuery('.egwGridView_scrollarea',this.div).css('height','auto');
2211
2212
							// Grid needs to redraw before it can be printed, so wait
2213
							window.setTimeout(jQuery.proxy(function() {
2214
								dialog.destroy();
2215
2216
								// Should be OK to print now
2217
								defer.resolve();
2218
							},nm),ET2_GRID_INVALIDATE_TIMEOUT);
2219
2220
						}
2221
2222
					},ctx);
2223
					count += 200;
2224
				} while (count < rows)
2225
				nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (rows+1));
2226
			}
2227
			else
2228
			{
2229
				// Don't need more rows, limit to requested and finish
2230
2231
				// Show it all
2232
				jQuery('.egwGridView_scrollarea',this.div).css('height','auto');
2233
2234
				// Use CSS to hide all but the requested rows
2235
				// Prevents us from showing more than requested, if actual height was less than average
2236
				this.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+"+rows+"))";
2237
				egw.css(this.print_row_selector, 'display: none');
2238
2239
				// No scrollbar in print view
2240
				jQuery('.egwGridView_scrollarea',this.div).css('overflow-y','hidden');
2241
				// Give dialog a chance to close, or it will be in the print
2242
				window.setTimeout(function() {defer.resolve();}, 0);
2243
			}
2244
		},this);
2245
2246
		var base_url = this.getInstanceManager().template_base_url;
2247
		if (base_url.substr(base_url.length - 1) == '/') base_url = base_url.slice (0, -1);	// otherwise we generate a url //api/templates, which is wrong
2248
		var dialog = et2_createWidget("dialog",{
2249
			// If you use a template, the second parameter will be the value of the template, as if it were submitted.
2250
			callback: callback,	// return false to prevent dialog closing
2251
			buttons: et2_dialog.BUTTONS_OK_CANCEL,
2252
			title: this.egw().lang('Print'),
2253
			template:this.egw().link(base_url+'/api/templates/default/nm_print_dialog.xet'),
2254
			value: {
2255
				content: {
2256
					row_count: Math.min(100,total),
2257
					columns: this.egw().preference(pref,app) || columns_selected
2258
				},
2259
				sel_options: {
2260
					columns: columns
2261
				}
2262
			}
2263
		});
2264
2265
		return defer;
2266
	},
2267
2268
	/**
2269
	 * Try to clean up the mess we made getting ready for printing
2270
	 * in beforePrint()
2271
	 */
2272
	afterPrint: function() {
2273
2274
		this.div.removeClass('print');
2275
2276
		// Put scrollbar back
2277
		jQuery('.egwGridView_scrollarea',this.div).css('overflow-y','');
2278
2279
		// Correct size of grid, and trigger resize to fix it
2280
		this.controller._grid.setScrollHeight(this.old_height);
2281
		delete this.old_height;
2282
2283
		// Remove CSS rule hiding extra rows
2284
		if(this.print_row_selector)
2285
		{
2286
			egw.css(this.print_row_selector, false);
2287
			delete this.print_row_selector;
2288
		}
2289
2290
		// Restore columns
2291
		var pref = [];
2292
		var app = this.getInstanceManager().app;
2293
		if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0)
2294
		{
2295
			pref = egw.preference(this.options.settings.columnselection_pref, app);
2296
		}
2297
		else
2298
		{
2299
			// 'nextmatch-' prefix is there in preference name, but not in setting, so add it in
2300
			pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app);
2301
		}
2302
		if(pref)
2303
		{
2304
			if(typeof pref === 'string') pref = pref.split(',');
2305
			this.set_columns(pref,app);
2306
		}
2307
		this.dynheight.outerNode.css('max-width','inherit');
2308
		this.resize();
2309
	}
2310
});}).call(this);
2311
et2_register_widget(et2_nextmatch, ["nextmatch"]);
2312
2313
/**
2314
 * Standard nextmatch header bar, containing filters, search, record count, letter filters, etc.
2315
 *
2316
 * Unable to use an existing template for this because parent (nm) doesn't, and template widget doesn't
2317
 * actually load templates from the server.
2318
 * @augments et2_DOMWidget
2319
 */
2320
var et2_nextmatch_header_bar = (function(){ "use strict"; return et2_DOMWidget.extend(et2_INextmatchHeader,
2321
{
2322
	attributes: {
2323
		"filter_label": {
2324
			"name": "Filter label",
2325
			"type": "string",
2326
			"description": "Label for filter",
2327
			"default": "",
2328
			"translate": true
2329
		},
2330
		"filter_help": {
2331
			"name": "Filter help",
2332
			"type": "string",
2333
			"description": "Help message for filter",
2334
			"default": "",
2335
			"translate": true
2336
		},
2337
		"filter": {
2338
			"name": "Filter value",
2339
			"type": "any",
2340
			"description": "Current value for filter",
2341
			"default": ""
2342
		},
2343
		"no_filter": {
2344
			"name": "No filter",
2345
			"type": "boolean",
2346
			"description": "Remove filter",
2347
			"default": false
2348
		}
2349
	},
2350
	headers: [],
2351
	header_div: [],
2352
2353
	/**
2354
	 * Constructor
2355
	 *
2356
	 * @param nextmatch
2357
	 * @param nm_div
2358
	 * @memberOf et2_nextmatch_header_bar
2359
	 */
2360
	init: function(nextmatch, nm_div) {
2361
		this._super.apply(this, [nextmatch,nextmatch.options.settings]);
2362
		this.nextmatch = nextmatch;
2363
		this.div = jQuery(document.createElement("div"))
2364
			.addClass("nextmatch_header");
2365
		this._createHeader();
2366
2367
		// Flag to avoid loops while updating filters
2368
		this.update_in_progress = false;
2369
	},
2370
2371
	destroy: function() {
2372
		this.nextmatch = null;
2373
2374
		this._super.apply(this, arguments);
2375
		this.div = null;
2376
	},
2377
2378
	setNextmatch: function(nextmatch) {
2379
		var create_once = (this.nextmatch == null);
2380
		this.nextmatch = nextmatch;
2381
		if(create_once)
2382
		{
2383
			this._createHeader();
2384
		}
2385
2386
		// Bind row count
2387
		this.nextmatch.dataview.grid.setInvalidateCallback(function () {
2388
			this.count_total.text(this.nextmatch.dataview.grid.getTotalCount() + "");
2389
		}, this);
2390
	},
2391
2392
	/**
2393
	 * Actions are handled by the controller, so ignore these
2394
	 *
2395
	 * @param {object} actions
2396
	 */
2397
	set_actions: function(actions) {},
2398
2399
	_createHeader: function() {
2400
2401
		var self = this;
2402
		var nm_div = this.nextmatch.div;
2403
		var settings = this.nextmatch.options.settings;
2404
2405
		this.div.prependTo(nm_div);
2406
2407
		// Left & Right (& row) headers
2408
		this.headers = [
2409
			{id:this.nextmatch.options.header_left},
2410
			{id:this.nextmatch.options.header_right},
2411
			{id:this.nextmatch.options.header_row}
2412
		];
2413
2414
		// The rest of the header
2415
		this.header_div = this.row_div = jQuery(document.createElement("div"))
2416
			.addClass("nextmatch_header_row")
2417
			.appendTo(this.div);
2418
		this.filter_div = jQuery(document.createElement("div"))
2419
			.addClass('filtersContainer')
2420
			.appendTo(this.row_div);
2421
2422
		// Search
2423
		this.search_box = jQuery(document.createElement("div"))
2424
			.addClass('search')
2425
			.prependTo(egwIsMobile()?this.nextmatch.div:this.row_div);
2426
2427
		// searchbox widget options
2428
		var searchbox_options = {
2429
			id:"search",
2430
			overlay:(typeof settings.searchbox != 'undefined' && typeof settings.searchbox.overlay != 'undefined')?settings.searchbox.overlay:false,
2431
			onchange:function(){
2432
				self.nextmatch.applyFilters({search: this.get_value()});
2433
			},
2434
			value:settings.search,
2435
			fix:!egwIsMobile()
2436
		};
2437
		// searchbox widget
2438
		this.et2_searchbox = et2_createWidget('searchbox', searchbox_options,this);
2439
2440
		// Set activeFilters to current value
2441
		this.nextmatch.activeFilters.search = settings.search;
2442
2443
		this.et2_searchbox.set_value(settings.search);
2444
		/**
2445
		 *  Mobile theme specific part for nm header
2446
		 *  nm header has very different behaivior for mobile theme and basically
2447
		 *  it has its own markup separately from nm header in normal templates.
2448
		 */
2449
		if (egwIsMobile())
2450
		{
2451
			this.search_box.addClass('nm-mob-header');
2452
			jQuery(this.div).css({display:'inline-block'}).addClass('nm_header_hide');
2453
2454
			//indicates appname in header
2455
			jQuery(document.createElement('div'))
2456
					.addClass('nm_appname_header')
2457
					.text(egw.lang(egw.app_name()))
2458
					.appendTo(this.search_box);
2459
2460
			this.delete_action = jQuery(document.createElement('div'))
2461
					.addClass('nm_delete_action')
2462
					.prependTo(this.search_box);
2463
			// toggle header
2464
			// add new button
2465
			this.fav_span = jQuery(document.createElement('div'))
2466
					.addClass('nm_favorites_div')
2467
					.prependTo(this.search_box);
2468
			// toggle header menu
2469
			this.toggle_header = jQuery(document.createElement('button'))
2470
					.addClass('nm_toggle_header')
2471
					.click(function(){
2472
						jQuery(self.div).toggleClass('nm_header_hide');
2473
						jQuery(this).toggleClass('nm_toggle_header_on');
2474
						window.setTimeout(function(){self.nextmatch.resize();},800);
2475
					})
2476
					.prependTo(this.search_box);
2477
			// Context menu
2478
			this.action_header = jQuery(document.createElement('button'))
2479
					.addClass('nm_action_header')
2480
					.hide()
2481
					.click (function(e){
2482
						jQuery('tr.selected',self.nextmatch.div).trigger({type:'contextmenu',which:3,originalEvent:e});
2483
					})
2484
					.prependTo(this.search_box);
2485
		}
2486
2487
		// Add category
2488
		if(!settings.no_cat) {
2489
			if (typeof settings.cat_id_label == 'undefined') settings.cat_id_label = '';
2490
			this.category = this._build_select('cat_id', settings.cat_is_select ? 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true);
2491
		}
2492
2493
		// Filter 1
2494
		if(!settings.no_filter) {
2495
			this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang);
2496
		}
2497
2498
		// Filter 2
2499
		if(!settings.no_filter2) {
2500
			this.filter2 = this._build_select('filter2', 'select', settings.filter2, settings.filter2_no_lang);
2501
		}
2502
2503
		// Other stuff
2504
		this.right_div = jQuery(document.createElement("div"))
2505
			.addClass('header_row_right').appendTo(this.row_div);
2506
2507
		// Record count
2508
		this.count = jQuery(document.createElement("span"))
2509
			.addClass("header_count ui-corner-all");
2510
2511
		// Need to figure out how to update this as grid scrolls
2512
		// this.count.append("? - ? ").append(egw.lang("of")).append(" ");
2513
		this.count_total = jQuery(document.createElement("span"))
2514
			.appendTo(this.count)
2515
			.text(settings.total + "");
2516
		this.count.prependTo(this.right_div);
2517
2518
		// Favorites
2519
		this._setup_favorites(settings['favorites']);
2520
2521
		// Export
2522
		if(typeof settings.csv_fields != "undefined" && settings.csv_fields != false)
2523
		{
2524
			var definition = settings.csv_fields;
2525
			if(settings.csv_fields === true)
2526
			{
2527
				definition = egw.preference('nextmatch-export-definition', this.nextmatch.egw().getAppName());
2528
			}
2529
			var button = et2_createWidget("buttononly", {id: "export", "label": "Export", image:"phpgwapi/filesave"}, this);
2530
			jQuery(button.getDOMNode())
2531
				.click(this.nextmatch, function(event) {
2532
					egw_openWindowCentered2( egw.link('/index.php', {
2533
						'menuaction':	'importexport.importexport_export_ui.export_dialog',
2534
						'appname':	event.data.egw().getAppName(),
2535
						'definition':	definition
2536
					}), '_blank', 850, 440, 'yes');
2537
				});
2538
		}
2539
2540
		// Another place to customize nextmatch
2541
		this.header_row = jQuery(document.createElement("div"))
2542
			.addClass('header_row').appendTo(this.right_div);
2543
2544
		// Letter search
2545
		var current_letter = this.nextmatch.options.settings.searchletter ?
2546
			this.nextmatch.options.settings.searchletter :
2547
			(this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false);
2548
		if(this.nextmatch.options.settings.lettersearch || current_letter)
2549
		{
2550
			this.lettersearch = jQuery(document.createElement("table"))
2551
				.css("width", "100%")
2552
				.appendTo(this.div);
2553
			var tbody = jQuery(document.createElement("tbody")).appendTo(this.lettersearch);
2554
			var row = jQuery(document.createElement("tr")).appendTo(tbody);
2555
2556
			// Capitals, A-Z
2557
			for(var i = 65; i <= 90; i++) {
2558
				var button = jQuery(document.createElement("td"))
2559
					.addClass("lettersearch")
2560
					.appendTo(row)
2561
					.attr("id", String.fromCharCode(i))
2562
					.text(String.fromCharCode(i));
2563
				if(String.fromCharCode(i) == current_letter) button.addClass("lettersearch_active");
2564
			}
2565
			button = jQuery(document.createElement("td"))
2566
				.addClass("lettersearch")
2567
				.appendTo(row)
2568
				.attr("id", "")
2569
				.text(egw.lang("all"));
2570
			if(!current_letter) button.addClass("lettersearch_active");
2571
2572
			this.lettersearch.click(this.nextmatch, function(event) {
2573
				// this is the lettersearch table
2574
				jQuery("td",this).removeClass("lettersearch_active");
2575
				jQuery(event.target).addClass("lettersearch_active");
2576
				event.data.applyFilters({searchletter: event.target.id || false});
2577
			});
2578
			// Set activeFilters to current value
2579
			this.nextmatch.activeFilters.searchletter = current_letter;
2580
		}
2581
		// Apply letter search preference
2582
		var lettersearch_preference = "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-lettersearch";
2583
		if(this.lettersearch && !egw.preference(lettersearch_preference,this.nextmatch.egw().getAppName()))
2584
		{
2585
			this.lettersearch.hide();
2586
		}
2587
	},
2588
2589
2590
	/**
2591
	 * Build & bind to a sub-template into the header
2592
	 *
2593
	 * @param {string} location One of left, right, or row
2594
	 * @param {string} template_name Name of the template to load into the location
2595
	 */
2596
	_build_header: function(location, template_name)
2597
	{
2598
		var id = location == "left" ? 0 : (location == "right" ? 1 : 2);
2599
		var existing = this.headers[id];
2600
		if(existing && existing._type)
2601
		{
2602
			if(existing.id == template_name) return;
2603
			existing.free();
2604
			this.headers[id] = '';
2605
		}
2606
2607
		// Load the template
2608
		var self = this;
2609
		var header = et2_createWidget("template", {"id": template_name}, this);
2610
		this.headers[id] = header;
2611
		var deferred = [];
2612
		header.loadingFinished(deferred);
2613
2614
		// Wait until all child widgets are loaded, then bind
2615
		jQuery.when.apply(jQuery,deferred).then(function() {
2616
			// fix order in DOM by reattaching templates in correct position
2617
			switch (id) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
2618
				case 0:	// header_left: prepend
2619
					jQuery(header.getDOMNode()).prependTo(self.header_div);
2620
					break;
2621
				case 1:	// header_right: before favorites and count
2622
					jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right'));
2623
					break;
2624
				case 2:	// header_row: after search
2625
					window.setTimeout(function(){	// otherwise we might end up after filters
2626
						jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search'));
2627
					}, 1);
2628
					break;
2629
			}
2630
			self._bindHeaderInput(header);
2631
		});
2632
	},
2633
2634
	/**
2635
	 * Build the selectbox filters in the header bar
2636
	 * Sets value, options, labels, and change handlers
2637
	 *
2638
	 * @param {string} name
2639
	 * @param {string} type
2640
	 * @param {string} value
2641
	 * @param {string} lang
2642
	 */
2643
	_build_select: function(name, type, value, lang) {
2644
		var widget_options = {
2645
			"id": name,
2646
			"label": this.nextmatch.options.settings[name+"_label"],
2647
			"no_lang": lang,
2648
			"disabled": this.nextmatch.options['no_'+name]
2649
		};
2650
2651
		// Set select options
2652
		// Check in content for options-<name>
2653
		var mgr = this.nextmatch.getArrayMgr("content");
2654
		var options = mgr.getEntry("options-" + name);
2655
		// Look in sel_options
2656
		if(!options) options = this.nextmatch.getArrayMgr("sel_options").getEntry(name);
2657
		// Check parent sel_options, because those are usually global and don't get passed down
2658
		if(!options) options = this.nextmatch.getArrayMgr("sel_options").parentMgr.getEntry(name);
2659
		// Sometimes legacy stuff puts it in here
2660
		if(!options) options = mgr.getEntry('rows[sel_options]['+name+']');
2661
2662
		// Maybe in a row, and options got stuck in ${row} instead of top level
2663
		var row_stuck = ['${row}','{$row}'];
2664
		for(var i = 0; !options && i < row_stuck.length; i++)
2665
		{
2666
			var row_id = '';
2667
			if((!options || options.length == 0) && (
2668
				// perspectiveData.row in nm, data["${row}"] in an auto-repeat grid
2669
				this.nextmatch.getArrayMgr("sel_options").perspectiveData.row || this.nextmatch.getArrayMgr("sel_options").data[row_stuck[i]]))
2670
			{
2671
				var row_id = name.replace(/[0-9]+/,row_stuck[i]);
2672
				options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id);
2673
				if(!options)
2674
				{
2675
					row_id = row_stuck[i] + "["+name+"]";
2676
					options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id);
2677
				}
2678
			}
2679
			if(options)
2680
			{
2681
				this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s".  Should be in sel_options[%s].',row_id,name);
2682
			}
2683
		}
2684
		if (name == 'cat_id')
2685
		{
2686
			jQuery.extend(widget_options, {
2687
				multiple: false,
2688
				tags: true,
2689
				class: "select-cat"
2690
			});
2691
		}
2692
		// Legacy: Add in 'All' option for cat_id, if not provided.
2693
		if(name == 'cat_id' && options != null && (typeof options[''] == 'undefined' && typeof options[0] != 'undefined' && options[0].value != ''))
2694
		{
2695
			widget_options.empty_label = this.egw().lang('All categories');
2696
		}
2697
2698
		// Create widget
2699
		var select = et2_createWidget(type, widget_options, this);
2700
2701
		if(options) select.set_select_options(options);
2702
2703
		// Set value
2704
		select.set_value(value);
2705
2706
		// Set activeFilters to current value
2707
		this.nextmatch.activeFilters[select.id] = select.get_value();
2708
2709
		// Set onChange
2710
		var input = select.input;
2711
2712
		// Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished()
2713
		select.attributes.select_options.ignore = true;
2714
2715
		if (this.nextmatch.options.settings[name+"_onchange"])
2716
		{
2717
			// Get the onchange function string
2718
			var onchange = this.nextmatch.options.settings[name+"_onchange"];
2719
2720
			// Real submits cause all sorts of problems
2721
			if(onchange.match(/this\.form\.submit/))
2722
			{
2723
				this.egw().debug("warn","%s tries to submit form, which is not allowed.  Filter changes automatically refresh data with no reload.",name);
2724
				onchange = onchange.replace(/this\.form\.submit\([^)]*\);?/,'return true;');
2725
			}
2726
2727
			// Connect it to the onchange event of the input element - may submit
2728
			select.change = et2_compileLegacyJS(onchange, this.nextmatch, select.getInputNode());
2729
			this._bindHeaderInput(select);
2730
		}
2731
		else	// default request changed rows with new filters, previous this.form.submit()
2732
		{
2733
			input.change(this.nextmatch, function(event) {
2734
				var set = {};
2735
				set[name] = select.getValue();
2736
				event.data.applyFilters(set);
2737
			});
2738
		}
2739
		return select;
2740
	},
2741
2742
	/**
2743
	 * Set up the favorites UI control
2744
	 *
2745
	 * @param filters Array|boolean The nextmatch setting for favorites.  Either true, or a list of
2746
	 *	additional fields/settings to add in to the favorite.
2747
	 */
2748
	_setup_favorites: function(filters) {
2749
		if(typeof filters == "undefined" || filters === false)
2750
		{
2751
			// No favorites configured
2752
			return;
2753
		}
2754
2755
		var list = et2_csvSplit(this.options.get_rows, 2, ".");
2756
		var widget_options = {
2757
			default_pref: "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-favorite",
2758
			app: list[0],
2759
			filters: filters,
2760
			sidebox_target:'favorite_sidebox_'+list[0]
2761
		};
2762
		this.favorites = et2_createWidget('favorites', widget_options, this);
2763
2764
		// Add into header
2765
		jQuery(this.favorites.getDOMNode(this.favorites)).prependTo(egwIsMobile()?this.search_box.find('.nm_favorites_div').show():this.right_div);
2766
	},
2767
2768
	/**
2769
	 * Updates all the filter elements in the header
2770
	 *
2771
	 * Does not actually refresh the data, just sets values to match those given.
2772
	 * Called by et2_nextmatch.applyFilters().
2773
	 *
2774
	 * @param filters Array Key => Value pairs of current filters
2775
	 */
2776
	setFilters: function(filters) {
2777
2778
		// Avoid loops cause by change events
2779
		if(this.update_in_progress) return;
2780
		this.update_in_progress = true;
2781
2782
		// Use an array mgr to hande non-simple IDs
2783
		var mgr = new et2_arrayMgr(filters);
2784
2785
		this.iterateOver(function(child) {
2786
			// Skip favorites, don't want them in the filter
2787
			if(typeof child.id != "undefined" && child.id.indexOf("favorite") == 0) return;
2788
2789
			var value = '';
2790
			if(typeof child.set_value != "undefined" && child.id)
2791
			{
2792
				value = mgr.getEntry(child.id);
2793
				if (value == null) value = '';
2794
				/**
2795
				 * Sometimes a filter value is not in current options.  This can
2796
				 * happen in a saved favorite, for example, or if server changes
2797
				 * some filter options, and the order doesn't work out.  The normal behaviour
2798
				 * is to warn & not set it, but for nextmatch we'll just add it
2799
				 * in, and let the server either set it properly, or ignore.
2800
				 */
2801
				if(value && typeof value != 'object' && child.instanceOf(et2_selectbox))
2802
				{
2803
					var found = typeof child.options.select_options[value] != 'undefined';
2804
					// options is array of objects with attribute value&label
2805
					if (jQuery.isArray(child.options.select_options))
2806
					{
2807
						for(var o=0; o < child.options.select_options.length; ++o)
2808
						{
2809
							if (child.options.select_options[o].value == value)
2810
							{
2811
								found = true;
2812
								break;
2813
							}
2814
						}
2815
					}
2816
					if (!found)
2817
					{
2818
						var old_options = child.options.select_options;
2819
						// Actual label is not available, obviously, or it would be there
2820
						old_options[value] = child.egw().lang("Loading");
2821
						child.set_select_options(old_options);
2822
					}
2823
				}
2824
				child.set_value(value);
2825
			}
2826
			if(typeof child.get_value == "function" && child.id)
2827
			{
2828
				// Put data in the proper place
2829
				var target = this;
2830
				var value = child.get_value();
2831
2832
				// Split up indexes
2833
				var indexes = child.id.replace(/&#x5B;/g,'[').split('[');
2834
2835
				for(var i = 0; i < indexes.length; i++)
2836
				{
2837
					indexes[i] = indexes[i].replace(/&#x5D;/g,'').replace(']','');
2838
					if (i < indexes.length-1)
2839
					{
2840
						if(typeof target[indexes[i]] == "undefined") target[indexes[i]] = {};
2841
						target = target[indexes[i]];
2842
					}
2843
					else
2844
					{
2845
						target[indexes[i]] = value;
2846
					}
2847
				}
2848
			}
2849
		}, filters);
2850
2851
		// Letter search
2852
		if(this.nextmatch.options.settings.lettersearch)
2853
		{
2854
			jQuery("td",this.lettersearch).removeClass("lettersearch_active");
2855
			jQuery(filters.searchletter ? "td#"+filters.searchletter : "td.lettersearch[id='']").addClass("lettersearch_active");
2856
2857
			// Set activeFilters to current value
2858
			filters.searchletter = jQuery("td.lettersearch_active").attr("id");
2859
		}
2860
2861
		// Reset flag
2862
		this.update_in_progress = false;
2863
	},
2864
2865
	/**
2866
	 * Help out nextmatch / widget stuff by checking to see if sender is part of header
2867
	 *
2868
	 * @param {et2_widget} _sender
2869
	 */
2870
	getDOMNode: function(_sender) {
2871
		var filters = [this.category, this.filter, this.filter2];
2872
		for(var i = 0; i < filters.length; i++)
2873
		{
2874
			if(_sender == filters[i])
2875
			{
2876
				// Give them the filter div
2877
				return this.filter_div[0];
2878
			}
2879
		}
2880
		if(_sender == this.et2_searchbox) return this.search_box[0];
2881
		if(_sender.id == 'export') return this.right_div[0];
2882
2883
		if(_sender && _sender._type == "template")
2884
		{
2885
			for(var i = 0; i < this.headers.length; i++)
2886
			{
2887
				if(_sender.id == this.headers[i].id && _sender._parent == this) return i == 2 ? this.header_row[0] : this.header_div[0];
2888
			}
2889
		}
2890
		return null;
2891
	},
2892
2893
	/**
2894
	 * Bind all the inputs in the header sub-templates to update the filters
2895
	 * on change, and update current filter with the inputs' current values
2896
	 *
2897
	 * @param {et2_template} sub_header
2898
	 */
2899
	_bindHeaderInput: function(sub_header) {
2900
		var header = this;
2901
2902
		var bind_change = function(_widget){
2903
			// Previously set change function
2904
			var widget_change = _widget.change;
2905
2906
			var change = function(_node) {
2907
				// Call previously set change function
2908
				var result = widget_change.call(_widget,_node);
2909
2910
				// Update filters, if we're not already doing so
2911
				if((result || typeof result === 'undefined') && _widget.isDirty() && !header.update_in_progress) {
2912
					// Update dirty
2913
					_widget._oldValue = _widget.getValue();
2914
2915
					// Widget will not have an entry in getValues() because nulls
2916
					// are not returned, we remove it from activeFilters
2917
					if(_widget._oldValue == null)
2918
					{
2919
						var path = _widget.getArrayMgr('content').explodeKey(_widget.id);
2920
						if(path.length > 0)
2921
						{
2922
							var entry = header.nextmatch.activeFilters;
2923
							var i = 0;
2924
							for(; i < path.length-1; i++)
2925
							{
2926
								entry = entry[path[i]];
2927
							}
2928
							delete entry[path[i]];
2929
						}
2930
						header.nextmatch.applyFilters(header.nextmatch.activeFilters);
2931
					}
2932
					else
2933
					{
2934
						// Not null is easy, just get values
2935
						var value = this.getInstanceManager().getValues(sub_header);
2936
						header.nextmatch.applyFilters(value[header.nextmatch.id]);
2937
					}
2938
				}
2939
				// In case this gets bound twice, it's important to return
2940
				return true;
2941
			};
2942
2943
			_widget.change = change;
2944
2945
			// Set activeFilters to current value
2946
			// Use an array mgr to hande non-simple IDs
2947
			var value = {};
2948
			value[_widget.id] = _widget._oldValue = _widget.getValue();
2949
			var mgr = new et2_arrayMgr(value);
2950
			jQuery.extend(true, this.nextmatch.activeFilters,mgr.data);
2951
		}
2952
		if(sub_header.instanceOf(et2_inputWidget))
2953
		{
2954
			bind_change.call(this, sub_header);
2955
		}
2956
		else
2957
		{
2958
			sub_header.iterateOver(bind_change, this, et2_inputWidget);
2959
		}
2960
	}
2961
});}).call(this);
2962
et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]);
2963
2964
/**
2965
 * Classes for the nextmatch sortheaders etc.
2966
 *
2967
 * @augments et2_baseWidget
2968
 */
2969
var et2_nextmatch_header = (function(){ "use strict"; return et2_baseWidget.extend(et2_INextmatchHeader,
2970
{
2971
	attributes: {
2972
		"label": {
2973
			"name": "Caption",
2974
			"type": "string",
2975
			"description": "Caption for the nextmatch header",
2976
			"translate": true
2977
		}
2978
	},
2979
2980
	/**
2981
	 * Constructor
2982
	 *
2983
	 * @memberOf et2_nextmatch_header
2984
	 */
2985
	init: function() {
2986
		this._super.apply(this, arguments);
2987
2988
		this.labelNode = jQuery(document.createElement("span"));
2989
		this.nextmatch = null;
2990
2991
		this.setDOMNode(this.labelNode[0]);
2992
	},
2993
2994
	destroy: function() {
2995
		this._super.apply(this, arguments);
2996
	},
2997
2998
	/**
2999
	 * Set nextmatch is the function which has to be implemented for the
3000
	 * et2_INextmatchHeader interface.
3001
	 *
3002
	 * @param {et2_nextmatch} _nextmatch
3003
	 */
3004
	setNextmatch: function(_nextmatch) {
3005
		this.nextmatch = _nextmatch;
3006
	},
3007
3008
	set_label: function(_value) {
3009
		this.label = _value;
3010
3011
		this.labelNode.text(_value);
3012
3013
		// add class if label is empty
3014
		this.labelNode.toggleClass('et2_label_empty', !_value);
3015
	}
3016
});}).call(this);
3017
et2_register_widget(et2_nextmatch_header, ['nextmatch-header']);
3018
3019
/**
3020
 * Extend header to process customfields
3021
 *
3022
 * @augments et2_customfields_list
3023
 */
3024
var et2_nextmatch_customfields = (function(){ "use strict"; return et2_customfields_list.extend(et2_INextmatchHeader,
3025
{
3026
	attributes: {
3027
		'customfields': {
3028
			'name': 'Custom fields',
3029
			'description': 'Auto filled'
3030
		},
3031
		'fields': {
3032
			'name': "Visible fields",
3033
			"description": "Auto filled"
3034
		}
3035
	},
3036
3037
	/**
3038
	 * Constructor
3039
	 *
3040
	 * @memberOf et2_nextmatch_customfields
3041
	 */
3042
	init: function() {
3043
		this.nextmatch = null;
3044
		this._super.apply(this, arguments);
3045
3046
		// Specifically take the whole column
3047
		this.table.css("width", "100%");
3048
	},
3049
3050
	destroy: function() {
3051
		this.nextmatch = null;
3052
		this._super.apply(this, arguments);
3053
	},
3054
3055
	transformAttributes: function(_attrs) {
3056
		this._super.apply(this, arguments);
3057
3058
		// Add in settings that are objects
3059
		if(!_attrs.customfields)
3060
		{
3061
			// Check for custom stuff (unlikely)
3062
			var data = this.getArrayMgr("modifications").getEntry(this.id);
3063
			// Check for global settings
3064
			if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true);
3065
			for(var key in data)
3066
			{
3067
				if(typeof data[key] === 'object' && ! _attrs[key]) _attrs[key] = data[key];
3068
			}
3069
		}
3070
	},
3071
3072
	setNextmatch: function(_nextmatch) {
3073
		this.nextmatch = _nextmatch;
3074
		this.loadFields();
3075
	},
3076
3077
	/**
3078
	 * Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio
3079
	 */
3080
	loadFields: function() {
3081
		if(this.nextmatch == null)
3082
		{
3083
			// not ready yet
3084
			return;
3085
		}
3086
		var columnMgr = this.nextmatch.dataview.getColumnMgr();
3087
		var nm_column = null;
3088
		var set_fields = {};
3089
		for(var i = 0; i < this.nextmatch.columns.length; i++)
3090
		{
3091
			if(this.nextmatch.columns[i].widget == this)
3092
			{
3093
				nm_column = columnMgr.columns[i];
3094
				break;
3095
			}
3096
		}
3097
		if(!nm_column) return;
3098
3099
		// Check for global setting changes (visibility)
3100
		var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~');
3101
		if(global_data != null && global_data.fields) this.options.fields = global_data.fields;
3102
3103
		var apps = egw.link_app_list();
3104
		for(var field_name in this.options.customfields)
3105
		{
3106
			var field = this.options.customfields[field_name];
3107
			var cf_id = et2_customfields_list.prototype.prefix + field_name;
3108
3109
3110
			if(this.rows[field_name]) continue;
3111
3112
			// Table row
3113
			var row = jQuery(document.createElement("tr"))
3114
					.appendTo(this.tbody);
3115
			var cf = jQuery(document.createElement("td"))
3116
					.appendTo(row);
3117
			this.rows[cf_id] = cf[0];
3118
3119
			// Create widget by type
3120
			var widget = null;
3121
			if(field.type == 'select' || field.type == 'select-account')
3122
			{
3123
				if(field.values && typeof field.values[''] !== 'undefined')
3124
				{
3125
					delete(field.values['']);
3126
				}
3127
				widget = et2_createWidget(
3128
					field.type == 'select-account' ? 'nextmatch-accountfilter' : "nextmatch-filterheader",
3129
					{
3130
						id: cf_id,
3131
						empty_label: field.label,
3132
						select_options: field.values
3133
					},
3134
					this
3135
				);
3136
			}
3137
			else if (apps[field.type])
3138
			{
3139
				widget = et2_createWidget("nextmatch-entryheader", {
3140
					id: cf_id,
3141
					only_app: field.type,
3142
					blur: field.label
3143
				}, this);
3144
			}
3145
			else
3146
			{
3147
				widget = et2_createWidget("nextmatch-sortheader", {
3148
					id: cf_id,
3149
					label: field.label
3150
				}, this);
3151
			}
3152
3153
			// If this is already attached, widget needs to be finished explicitly
3154
			if(this.isAttached() && !widget.isAttached())
3155
			{
3156
				widget.loadingFinished();
3157
			}
3158
			// Check for column filter
3159
			if(!jQuery.isEmptyObject(this.options.fields) && (
3160
				this.options.fields[field_name] == false || typeof this.options.fields[field_name] == 'undefined'))
3161
			{
3162
				cf.hide();
3163
			}
3164
			else if (jQuery.isEmptyObject(this.options.fields))
3165
			{
3166
				// If we're showing it make sure it's set, but only after
3167
				set_fields[field_name] = true;
3168
			}
3169
		}
3170
		jQuery.extend(this.options.fields, set_fields);
3171
	},
3172
3173
	/**
3174
	 * Override parent so we can update the nextmatch row too
3175
	 *
3176
	 * @param {array} _fields
3177
	 */
3178
	set_visible: function(_fields) {
3179
		this._super.apply(this, arguments);
3180
3181
		// Find data row, and do it too
3182
		var self = this;
3183
		if(this.nextmatch)
3184
		{
3185
			this.nextmatch.iterateOver(
3186
				function(widget) {
3187
					if(widget == self) return;
3188
					widget.set_visible(_fields);
3189
				}, this, et2_customfields_list
3190
			);
3191
		}
3192
	},
3193
3194
	/**
3195
	 * Provide own column caption (column selection)
3196
	 *
3197
	 * If only one custom field, just use that, otherwise use "custom fields"
3198
	 */
3199
	_genColumnCaption: function() {
3200
		return egw.lang("Custom fields");
3201
	},
3202
3203
	/**
3204
	 * Provide own column naming, including only selected columns - only useful
3205
	 * to nextmatch itself, not for sending server-side
3206
	 */
3207
	_getColumnName: function() {
3208
		var name = this.id;
3209
		var visible = [];
3210
		for(var field_name in this.options.customfields)
3211
		{
3212
			if(jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true)
3213
			{
3214
				visible.push(et2_customfields_list.prototype.prefix + field_name);
3215
				jQuery(this.rows[field_name]).show();
3216
			}
3217
			else if (typeof this.rows[field_name] != "undefined")
3218
			{
3219
				jQuery(this.rows[field_name]).hide();
3220
			}
3221
		}
3222
3223
		if(visible.length) {
3224
			name  +="_"+ visible.join("_");
3225
		}
3226
		else
3227
		{
3228
			// None hidden means all visible
3229
			jQuery(this.rows[field_name]).parent().parent().children().show();
3230
		}
3231
3232
		// Update global custom fields column(s) - widgets will check on their own
3233
3234
		// Check for custom stuff (unlikely)
3235
		var data = this.getArrayMgr("modifications").getEntry(this.id);
3236
		// Check for global settings
3237
		if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true) || {};
3238
		if(!data.fields) data.fields = {};
3239
		for(var field in this.options.customfields)
3240
		{
3241
			data.fields[field] = (this.options.fields == null || typeof this.options.fields[field] == 'undefined' ? false : this.options.fields[field]);
3242
		}
3243
		return name;
3244
	}
3245
});}).call(this);
3246
et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']);
3247
3248
/**
3249
 * @augments et2_nextmatch_header
3250
 */
3251
var et2_nextmatch_sortheader = (function(){ "use strict"; return et2_nextmatch_header.extend(et2_INextmatchSortable,
3252
{
3253
	attributes: {
3254
		"sortmode": {
3255
			"name": "Sort order",
3256
			"type": "string",
3257
			"description": "Default sort order",
3258
			"translate": false
3259
		}
3260
	},
3261
	legacyOptions: ['sortmode'],
3262
3263
	/**
3264
	 * Constructor
3265
	 *
3266
	 * @memberOf et2_nextmatch_sortheader
3267
	 */
3268
	init: function() {
3269
		this._super.apply(this, arguments);
3270
3271
		this.sortmode = "none";
3272
3273
		this.labelNode.addClass("nextmatch_sortheader none");
3274
	},
3275
3276
	click: function() {
3277
		if (this.nextmatch && this._super.apply(this, arguments))
3278
		{
3279
			// Send default sort mode if not sorted, otherwise send undefined to calculate
3280
			this.nextmatch.sortBy(this.id, this.sortmode == "none" ? !(this.options.sortmode.toUpperCase() == "DESC") : undefined);
3281
			return true;
3282
		}
3283
3284
		return false;
3285
	},
3286
3287
	/**
3288
	 * Wrapper to join up interface * framework
3289
	 *
3290
	 * @param {string} _mode
3291
	 */
3292
	set_sortmode: function(_mode)
3293
	{
3294
		// Set via nextmatch after setup
3295
		if(this.nextmatch) return;
3296
3297
		this.setSortmode(_mode);
3298
	},
3299
3300
	/**
3301
	 * Function which implements the et2_INextmatchSortable function.
3302
	 *
3303
	 * @param {string} _mode
3304
	 */
3305
	setSortmode: function(_mode) {
3306
		// Remove the last sortmode class and add the new one
3307
		this.labelNode.removeClass(this.sortmode)
3308
			.addClass(_mode);
3309
3310
		this.sortmode = _mode;
3311
	}
3312
3313
});}).call(this);
3314
et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']);
3315
3316
/**
3317
 * @augments et2_selectbox
3318
 */
3319
var et2_nextmatch_filterheader = (function(){ "use strict"; return et2_selectbox.extend([et2_INextmatchHeader, et2_IResizeable],
3320
{
3321
	/**
3322
	 * Override to add change handler
3323
	 *
3324
	 * @memberOf et2_nextmatch_filterheader
3325
	 */
3326
	createInputWidget: function() {
3327
		// Make sure there's an option for all
3328
		if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""]))
3329
		{
3330
			this.options.empty_label = this.options.label ? this.options.label : egw.lang("All");
3331
		}
3332
		this._super.apply(this, arguments);
3333
3334
		this.input.change(this, function(event) {
3335
			if(typeof event.data.nextmatch == 'undefined')
3336
			{
3337
				// Not fully set up yet
3338
				return;
3339
			}
3340
			var col_filter = {};
3341
			col_filter[event.data.id] = event.data.input.val();
3342
			// Set value so it's there for response (otherwise it gets cleared if options are updated)
3343
			event.data.set_value(event.data.input.val());
3344
3345
			event.data.nextmatch.applyFilters({col_filter: col_filter});
3346
		});
3347
3348
	},
3349
3350
	/**
3351
	 * Set nextmatch is the function which has to be implemented for the
3352
	 * et2_INextmatchHeader interface.
3353
	 *
3354
	 * @param {et2_nextmatch} _nextmatch
3355
	 */
3356
	setNextmatch: function(_nextmatch) {
3357
		this.nextmatch = _nextmatch;
3358
3359
		// Set current filter value from nextmatch settings
3360
		if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined")
3361
		{
3362
			this.set_value(this.nextmatch.activeFilters.col_filter[this.id]);
3363
3364
			// Make sure it's set in the nextmatch
3365
			_nextmatch.activeFilters.col_filter[this.id] = this.getValue();
3366
		}
3367
	},
3368
3369
	// Make sure selectbox is not longer than the column
3370
	resize: function() {
3371
		this.input.css("max-width",jQuery(this.parentNode).innerWidth() + "px");
3372
	}
3373
3374
});}).call(this);
3375
et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']);
3376
3377
/**
3378
 * @augments et2_selectAccount
3379
 */
3380
var et2_nextmatch_accountfilterheader = (function(){ "use strict"; return et2_selectAccount.extend([et2_INextmatchHeader, et2_IResizeable],
3381
{
3382
	/**
3383
	 * Override to add change handler
3384
	 *
3385
	 * @memberOf et2_nextmatch_accountfilterheader
3386
	 */
3387
	createInputWidget: function() {
3388
		// Make sure there's an option for all
3389
		if(!this.options.empty_label && !this.options.select_options[""])
3390
		{
3391
			this.options.empty_label = this.options.label ? this.options.label : egw.lang("All");
3392
		}
3393
		this._super.apply(this, arguments);
3394
3395
		this.input.change(this, function(event) {
3396
			if(typeof event.data.nextmatch == 'undefined')
3397
			{
3398
				// Not fully set up yet
3399
				return;
3400
			}
3401
			var col_filter = {};
3402
			col_filter[event.data.id] = event.data.getValue();
3403
			event.data.nextmatch.applyFilters({col_filter: col_filter});
3404
		});
3405
3406
	},
3407
3408
	/**
3409
	 * Set nextmatch is the function which has to be implemented for the
3410
	 * et2_INextmatchHeader interface.
3411
	 *
3412
	 * @param {et2_nextmatch} _nextmatch
3413
	 */
3414
	setNextmatch: function(_nextmatch) {
3415
		this.nextmatch = _nextmatch;
3416
3417
		// Set current filter value from nextmatch settings
3418
		if(this.nextmatch.activeFilters.col_filter && this.nextmatch.activeFilters.col_filter[this.id])
3419
		{
3420
			this.set_value(this.nextmatch.activeFilters.col_filter[this.id]);
3421
		}
3422
	},
3423
	// Make sure selectbox is not longer than the column
3424
	resize: function() {
3425
		var max = jQuery(this.parentNode).innerWidth() - 4;
3426
		var surroundings = this.getSurroundings()._widgetSurroundings;
3427
		for(var i = 0; i < surroundings.length; i++)
3428
		{
3429
			max -= jQuery(surroundings[i]).outerWidth();
3430
		}
3431
		this.input.css("max-width",max + "px");
3432
	}
3433
3434
});}).call(this);
3435
et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']);
3436
3437
/**
3438
 * Filter allowing multiple values to be selected, base on a taglist instead
3439
 * of a regular selectbox
3440
 *
3441
 * @augments et2_taglist
3442
 */
3443
var et2_nextmatch_taglistheader = (function(){ "use strict"; return et2_taglist.extend([et2_INextmatchHeader, et2_IResizeable],
3444
{
3445
	attributes: {
3446
		autocomplete_url: { default: ''},
3447
		multiple: { default: 'toggle'},
3448
		onchange: {
3449
			default: function(event) {
3450
				if(typeof this.nextmatch === 'undefined')
3451
				{
3452
					// Not fully set up yet
3453
					return;
3454
				}
3455
				var col_filter = {};
3456
				col_filter[this.id] = this.getValue();
3457
				// Set value so it's there for response (otherwise it gets cleared if options are updated)
3458
				//event.data.set_value(event.data.input.val());
3459
3460
				this.nextmatch.applyFilters({col_filter: col_filter});
3461
			}
3462
		},
3463
		rows: { default: 2},
3464
		class: {default: 'nm_filterheader_taglist'}
3465
	},
3466
3467
	/**
3468
	 * Override to add change handler
3469
	 *
3470
	 * @memberOf et2_nextmatch_filterheader
3471
	 */
3472
	createInputWidget: function() {
3473
		// Make sure there's an option for all
3474
		if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""]))
3475
		{
3476
			this.options.empty_label = this.options.label ? this.options.label : egw.lang("All");
3477
		}
3478
		this._super.apply(this, arguments);
3479
	},
3480
3481
	/**
3482
	 * Disable toggle if there are 2 or less options
3483
	 * @param {Object[]} options
3484
	 */
3485
	set_select_options: function(options)
3486
	{
3487
		if(options && options.length <= 2 && this.options.multiple == 'toggle')
3488
		{
3489
			this.set_multiple(false);
3490
		}
3491
		this._super.apply(this, arguments);
3492
	},
3493
3494
	/**
3495
	 * Set nextmatch is the function which has to be implemented for the
3496
	 * et2_INextmatchHeader interface.
3497
	 *
3498
	 * @param {et2_nextmatch} _nextmatch
3499
	 */
3500
	setNextmatch: function(_nextmatch) {
3501
		this.nextmatch = _nextmatch;
3502
3503
		// Set current filter value from nextmatch settings
3504
		if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined")
3505
		{
3506
			this.set_value(this.nextmatch.activeFilters.col_filter[this.id]);
3507
3508
			// Make sure it's set in the nextmatch
3509
			_nextmatch.activeFilters.col_filter[this.id] = this.getValue();
3510
		}
3511
	},
3512
3513
	// Make sure selectbox is not longer than the column
3514
	resize: function() {
3515
		this.div.css("height",'');
3516
		this.div.css("max-width",jQuery(this.parentNode).innerWidth() + "px");
3517
		this._super.apply(this, arguments);
3518
	}
3519
3520
});}).call(this);
3521
et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']);
3522
3523
/**
3524
 * Filter allowing multiple values to be selected, base on a taglist instead
3525
 * of a regular selectbox
3526
 *
3527
 * @augments et2_taglist
3528
 */
3529
var et2_nextmatch_taglistheader = (function(){ "use strict"; return et2_taglist.extend([et2_INextmatchHeader, et2_IResizeable],
3530
{
3531
	attributes: {
3532
		autocomplete_url: { default: ''},
3533
		multiple: { default: 'toggle'},
3534
		onchange: {
3535
			default: function(event) {
3536
				if(typeof this.nextmatch === 'undefined')
3537
				{
3538
					// Not fully set up yet
3539
					return;
3540
				}
3541
				var col_filter = {};
3542
				col_filter[this.id] = this.getValue();
3543
				// Set value so it's there for response (otherwise it gets cleared if options are updated)
3544
				//event.data.set_value(event.data.input.val());
3545
3546
				this.nextmatch.applyFilters({col_filter: col_filter});
3547
			}
3548
		},
3549
		rows: { default: 2},
3550
		class: {default: 'nm_filterheader_taglist'}
3551
	},
3552
3553
	/**
3554
	 * Override to add change handler
3555
	 *
3556
	 * @memberOf et2_nextmatch_filterheader
3557
	 */
3558
	createInputWidget: function() {
3559
		// Make sure there's an option for all
3560
		if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""]))
3561
		{
3562
			this.options.empty_label = this.options.label ? this.options.label : egw.lang("All");
3563
		}
3564
		this._super.apply(this, arguments);
3565
	},
3566
3567
	/**
3568
	 * Disable toggle if there are 2 or less options
3569
	 * @param {Object[]} options
3570
	 */
3571
	set_select_options: function(options)
3572
	{
3573
		if(options && options.length <= 2 && this.options.multiple == 'toggle')
3574
		{
3575
			this.set_multiple(false);
3576
		}
3577
		this._super.apply(this, arguments);
3578
	},
3579
3580
	/**
3581
	 * Set nextmatch is the function which has to be implemented for the
3582
	 * et2_INextmatchHeader interface.
3583
	 *
3584
	 * @param {et2_nextmatch} _nextmatch
3585
	 */
3586
	setNextmatch: function(_nextmatch) {
3587
		this.nextmatch = _nextmatch;
3588
3589
		// Set current filter value from nextmatch settings
3590
		if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined")
3591
		{
3592
			this.set_value(this.nextmatch.activeFilters.col_filter[this.id]);
3593
3594
			// Make sure it's set in the nextmatch
3595
			_nextmatch.activeFilters.col_filter[this.id] = this.getValue();
3596
		}
3597
	},
3598
3599
	// Make sure selectbox is not longer than the column
3600
	resize: function() {
3601
		this.div.css("height",'');
3602
		this.div.css("max-width",jQuery(this.parentNode).innerWidth() + "px");
3603
		this._super.apply(this, arguments);
3604
	}
3605
3606
});}).call(this);
3607
et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']);
3608
3609
/**
3610
 * @augments et2_link_entry
3611
 */
3612
var et2_nextmatch_entryheader = (function(){ "use strict"; return et2_link_entry.extend(et2_INextmatchHeader,
3613
{
3614
	/**
3615
	 * Override to add change handler
3616
	 *
3617
	 * @memberOf et2_nextmatch_entryheader
3618
	 * @param {object} event
3619
	 * @param {object} selected
3620
	 */
3621
	onchange: function(event, selected) {
3622
		var col_filter = {};
3623
		col_filter[this.id] = this.get_value();
3624
		this.nextmatch.applyFilters.call(this.nextmatch, {col_filter: col_filter});
3625
	},
3626
3627
	/**
3628
	 * Override to always return a string appname:id (or just id) for simple (one real selection)
3629
	 * cases, parent returns an object.  If multiple are selected, or anything other than app and
3630
	 * id, the original parent value is returned.
3631
	 */
3632
	getValue: function() {
3633
		var value = this._super.apply(this, arguments);
3634
		if(typeof value == "object" && value != null)
3635
		{
3636
			if(!value.app || !value.id) return null;
3637
3638
			// If array with just one value, use a string instead for legacy server handling
3639
			if(typeof value.id == 'object' && value.id.shift && value.id.length == 1)
3640
			{
3641
				value.id = value.id.shift();
3642
			}
3643
			// If simple value, format it legacy string style, otherwise
3644
			// we return full value
3645
			if(typeof value.id == 'string')
3646
			{
3647
				value = value.app +":"+value.id;
3648
			}
3649
		}
3650
		return value;
3651
	},
3652
3653
	/**
3654
	 * Set nextmatch is the function which has to be implemented for the
3655
	 * et2_INextmatchHeader interface.
3656
	 *
3657
	 * @param {et2_nextmatch} _nextmatch
3658
	 */
3659
	setNextmatch: function(_nextmatch) {
3660
		this.nextmatch = _nextmatch;
3661
3662
		// Set current filter value from nextmatch settings
3663
		if(this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id])
3664
		{
3665
			this.set_value(this.nextmatch.options.settings.col_filter[this.id]);
3666
3667
			if(this.getValue() != this.nextmatch.activeFilters.col_filter[this.id])
3668
			{
3669
				this.nextmatch.activeFilters.col_filter[this.id] = this.getValue();
3670
			}
3671
3672
			// Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished()
3673
			this.attributes.value.ignore = true;
3674
			//this.attributes.select_options.ignore = true;
3675
		}
3676
		var self = this;
3677
		// Fire on lost focus, clear filter if user emptied box
3678
	}
3679
});}).call(this);
3680
et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']);
3681
3682
/**
3683
 * @augments et2_nextmatch_filterheader
3684
 */
3685
var et2_nextmatch_customfilter = (function(){ "use strict"; return et2_nextmatch_filterheader.extend(
3686
{
3687
	attributes: {
3688
		"widget_type": {
3689
			"name": "Actual type",
3690
			"type": "string",
3691
			"description": "The actual type of widget you should use",
3692
			"no_lang": 1
3693
		},
3694
		"widget_options": {
3695
			"name": "Actual options",
3696
			"type": "any",
3697
			"description": "The options for the actual widget",
3698
			"no_lang": 1,
3699
			"default": {}
3700
		}
3701
	},
3702
	legacyOptions: ["widget_type","widget_options"],
3703
3704
	real_node: null,
3705
3706
	/**
3707
	 * Constructor
3708
	 *
3709
	 * @param _parent
3710
	 * @param _attrs
3711
	 * @memberOf et2_nextmatch_customfilter
3712
	 */
3713
	init: function(_parent, _attrs) {
3714
3715
		switch(_attrs.widget_type)
3716
		{
3717
			case "link-entry":
3718
				_attrs.type = 'nextmatch-entryheader';
3719
				break;
3720
			default:
3721
				if(_attrs.widget_type.indexOf('select') === 0)
3722
				{
3723
					_attrs.type = 'nextmatch-filterheader';
3724
				}
3725
				else
3726
				{
3727
					_attrs.type = _attrs.widget_type;
3728
				}
3729
		}
3730
		jQuery.extend(_attrs.widget_options,{id: this.id});
3731
3732
		_attrs.id = '';
3733
		this._super.apply(this, arguments);
3734
		this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, this._parent);
3735
		var select_options = [];
3736
		var correct_type = _attrs.type;
3737
		this.real_node._type = _attrs.widget_type;
3738
		et2_selectbox.find_select_options(this.real_node, select_options, _attrs);
3739
		this.real_node._type = correct_type;
3740
		if(typeof this.real_node.set_select_options === 'function')
3741
		{
3742
			this.real_node.set_select_options(select_options);
3743
		}
3744
	},
3745
3746
	// Just pass the real DOM node through, in case anybody asks
3747
	getDOMNode: function(_sender) {
3748
		return this.real_node ? this.real_node.getDOMNode(_sender) : null;
3749
	},
3750
3751
	// Also need to pass through real children
3752
	getChildren: function() {
3753
		return this.real_node.getChildren() || [];
3754
	},
3755
	setNextmatch: function(_nextmatch)
3756
	{
3757
		if(this.real_node && this.real_node.setNextmatch)
3758
		{
3759
			return this.real_node.setNextmatch(_nextmatch);
3760
		}
3761
	}
3762
});}).call(this);
3763
et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']);
3764